Commit Graph
554 Commits
Author SHA1 Message Date
zeekay 588a790b4b Regenerate against the current cloud document
The lock sat at 574d1f1d while the product moved on. The team surface
stopped at rooms — no messages path, no way to open one — so anything
built on this client had to reach past it with its own HTTP calls, which
is the duplication a generated client exists to prevent.
2026-09-01 02:21:39 -07:00
zeekay c7355b81fe Take the catalog cloud now writes 2026-08-31 23:42:47 -07:00
zeekay eab5541a89 Stop handing a model the secrets the fleet withholds
Nine per-service packages registered a hanzo.tools entry point and sat in
hanzo-mcp's default dependencies, so installing it put them in a model's tool
list beside the unified tool and without its filter.

hanzo-tools-kms offered inject, which lists a path and reads every secret at it.
hanzo-tools-iam offered create_user, delete_user and the token and session reads.
cloud withholds all of them from an agent; these were a second way to the same
API, so the rule held on one path and not the other.

The auto-registration is withdrawn, not the packages: they stay importable for a
human at a CLI, which is where reading a secret is a legitimate act. An agent
reaches all eight through the unified tool, verified against the live document.

mpc keeps its registration because the fleet has no equivalent, and auth keeps
its because it reads the caller's own claims rather than another identity's.
2026-08-30 20:08:02 -07:00
zeekay 42139d1849 Offer what the fleet offers, not what the contract lists
The unified tool derived its surface from /v1/openapi.json, which describes the
API rather than what a model may be handed. cloud keeps an operation off the
agent surface when its name discloses a bearer secret or when a mutating verb
acts on identity — post_iam_users is in the contract and refused at /v1/mcp, so
this tool offered an identity mutation the fleet declines.

The document still supplies each operation's method and path, which a caller
needs to make the request; the generated catalog supplies which are offered. Two
facts, one source each, and the rule is asked of cloud rather than restated. An
unreadable catalog offers nothing, because failing open widens the surface
silently.

Two aliases pointed at names the fleet had left behind, mapping callers off the
live surface. A synonym may only point at a name it serves.

A fixture naming no operations is not exercising the offer rule and now says so,
rather than the default being weakened to keep it green.
2026-08-30 19:52:51 -07:00
zeekay badff3db49 Read the fleet from the same catalog the other runtimes do
Python addressed the cloud through ten hand-written packages while TypeScript and
Rust read a generated catalog, and the hand-written ones had drifted: paas is
platform, storage is s3, auth folded into iam, and commerce asked for plurals the
fleet does not publish.

hanzo-tools-cloud embeds the file those two already embed, so the three cannot
disagree about what the API offers — a test compares them by digest. One routing
tool over 114 subsystems rather than 114 tools, for the reason the fleet groups
its own.

Naming a subsystem the fleet does not serve is answered before the credential
check: demanding a key for something that does not exist sends the caller to fix
the wrong thing.
2026-08-30 17:05:32 -07:00
zeekay b4b0f3bf67 Ask commerce for what commerce serves
The base was api.hanzo.ai/api/v1 and then /v1; both 404. The surface is under
/v1/commerce, and the fleet names each resource in the singular — product,
collection, store, discount, webhook — so every read here asked for a plural that
does not exist.

Orders and users have no address at all: an order is reached through the store
that holds it, and a user is IAM's. Both now say which, because a 404 reads to an
agent as an empty result rather than as a wrong question.
2026-08-30 16:42:08 -07:00
zeekay 9194042947 Merge remote-tracking branch 'origin/main' 2026-08-30 16:29:35 -07:00
zeekay b1ab1198c1 Address the estate at /v1, not /api/v1
Two defaults carried an /api/ prefix this estate does not serve: the commerce
base and the DNS endpoint.

Neither resource answers at either spelling today — api.hanzo.ai serves commerce
carts under /v1/commerce and none of the products or orders this tool asks for,
and dns.hanzo.ai answers nothing at all. The prefix is the part that was wrong
either way; what those tools reach is a separate question.
2026-08-30 16:29:33 -07:00
zeekay 90e00d9bb7 State a version once
A version written into a manifest is written into every manifest that wants it,
which is how eslint came to exist in a hundred and four forms across this estate
and how a package came to resolve twice at run time. The catalog moves the number
out of the manifest: a manifest names the package, the workspace names the
version.

Four are held below their latest because the newer release cannot be used, and
the reason is recorded rather than the version alone: @noble/hashes 2 renamed
every subpath the web3 stack imports, @splinetool/runtime 2 depends on a package
that was never published, @dynamic-labs 5.4.1 the same, and react-resizable-panels
3 points its types condition at the bundle.
2026-08-29 13:46:58 -07:00
zeekayandHanzo Dev 38c2c0eb74 Read both wire versions, the way the canonical parser does
zap.wire.parse admits versions 1 and 2, which is the set Go's zap.Parse admits;
this layer admitted only the one it emits, so a frame from luxd — which emits 2 —
was refused by a reader sitting on top of a parser that would have taken it. A
reader that refuses what the network sends is a different protocol.

Accepting both changes nothing about what this layer emits.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-29 13:09:36 -07:00
zeekay 4662534cd9 Regenerate: the AI surface says what it answers
288 of hanzoai/ai's 294 operations now declare a response, against 1 before.
GET /v1/models, POST /v1/chat/completions, /v1/embeddings, /v1/messages —
every address a client actually calls — used to reach this client untyped,
because cloud's relay carried an operation's sentence and dropped the shapes
it named. It carries both now.

Fleet-wide the document goes from 69% to 81% of operations declaring what
comes back, which is where the ~208 new models here come from.
2026-08-28 15:53:34 -07:00
zeekay 145e9f1c68 regenerate from cloud 529be4ccb9 2026-08-28 08:10:42 -07:00
zeekay c0e89d2ad6 regenerate from cloud 92604dd76f 2026-08-27 20:41:31 -07:00
zeekay e31dd96f46 regenerate from cloud a48ddfb315 2026-08-27 19:44:26 -07:00
zeekay 8d99a341b9 regenerate from cloud 16e3b9cef1 2026-08-27 17:24:00 -07:00
zeekay a303b73f19 regenerate from cloud 03cd1b9b10 2026-08-27 17:04:47 -07:00
zeekay 8e4669c68e regenerate from cloud 733acc2f6d 2026-08-27 16:09:11 -07:00
zeekay 2a55a3e2c8 regenerate from cloud 3f82fe0da0 2026-08-27 15:19:38 -07:00
zeekay 7c4b937533 regenerate from cloud 186c87db6f 2026-08-27 13:52:35 -07:00
zeekay 369b525882 fix(tools): a screenshot arrives downscaled, with the full capture on disk 2026-08-27 08:45:44 -07:00
zeekay 9f6663482d regenerate from cloud 939b09d4
The rebase folded the fleet-wide projection into the newer v8.5.151 already on
main, and two files did not survive it: git_api and websearch_api still
described the previous emission. .spec-lock came back conflicted from the same
rebase and is set to the document these were generated from.

`generate.py python --check` is clean after.
2026-08-24 23:38:24 -07:00
zeekayandHanzo Dev dc28d4b761 hanzoai.cloud: project v8.5.151, where the last retired IAM addresses left
.spec-lock named hanzo-inc/cloud@54ddf3d6, whose document still published nine
verb-noun IAM addresses — get-account, get-app-login, delete-mfa,
issue-user-token, mint-user-keys, revoke-user-keys, send-verification-code,
set-preferred-mfa, update-preferences. Every one answers 410 with a Link naming
its successor, so every generated method for them was a call that cannot land.
v8.5.151 (6437e8bb) drops them; measured against api.hanzo.ai/v1/openapi.json it
carries the identical 2253 operations over 1612 paths, zero either way. The only
verb-noun left is /v1/iam/delete-membership, which is served and published.

The smoke tests named four surfaces the pinned document had already stopped
carrying, and were failing before this change: KeysApi (the keys tag folded into
iam), ModelsApi (/v1/models no longer declares security: [], while
/v1/openapi.json has carried the marking across every pin), analytics and chat,
and admin plugins (/v1/tools/plugins is where the published plugin surface is).
Each now names what the document holds, and the breadth floor counts tags rather
than tracking a number that moves with every re-tag.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 03:42:28 -07:00
zeekayandHanzo Dev d6ecee069c iam: pass the scope through, and let the server's refusal speak
Which list routes insist on an owner is the server's rule, and it moves. The
deployed binary requires one on users, sessions and applications and not on
roles, permissions, providers, workspaces, invitations, tokens or audit-logs;
the change that resolves a listing's tenant centrally is written but unreleased.

So nothing here keeps a copy of that rule. The IAM tool no longer refuses a
missing owner on applications and sessions, get_applications takes an owner
rather than demanding one, and no docstring or parameter description promises
that leaving it off makes the credential decide — today that answers 400.

A refusal now reaches the caller as the reason IAM gave rather than as a
transcribed body: the RFC 9457 detail, or the envelope's msg where the route
answers the envelope, which is how memberships refuses.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 03:42:08 -07:00
zeekayandHanzo Dev 95e2025681 iam: address the rows IAM serves, and read the shape each route answers
A row is named by owner and name — /v1/iam/users/hanzo/z — read with GET,
replaced with PUT, removed with DELETE. The hand-written clients reached for
verb-noun addresses, and the CLI and route table reached for a verb hanging off
the collection; both spellings are gone from the server.

A collection GET answers a wrapper keyed by the plural, and the key is not
always the word in the path — audit-logs answers auditLogs. Every reader now
takes the key from the table and raises when it is absent, because reading a
missing key as [] reports "none" for a healthy org. A row answers the record
itself, absence is 404, and a refusal is an RFC 9457 problem document carrying
the reason. account, memberships, password and login keep the {status, msg,
data} envelope and have their own reader.

The credential goes in Authorization as client_secret_basic, which is where IAM
reads one; the pair used to ride the query string, where it authenticated
nothing and landed in every log on the path. A password rides beside the user
row rather than inside it, so IAM hashes it and nothing writes it in the clear.
An update reads the row before replacing it, since PUT replaces the whole
record.

Role membership is the role's own users list, so adding and removing a member
reads the role, edits the list and writes it back.

The IAM tool drops enforce, system_info and health; the team tool reaches
IAM's workspaces, memberships, invitations and account instead of deferring to
an address it never had. Both name organizations only when the caller does —
IAM scopes the read, and a hardcoded owner is a refusal for everyone outside it.

The CLI route table is checked against IAM's own registered routes, and every
address here was verified against the running server.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-24 03:36:48 -07:00
zeekay 53cd11b0a5 hanzo-iam: one decoder, so a wrong path names itself
hanzo.id serves its sign-in SPA on every unmatched path, so a path IAM
does not serve answers 200 text/html. Forty-four call sites went straight
to .json() on the strength of the status code and died inside it with a
byte offset naming neither the path nor the reason. oauth was the one
module that asked, in a guard of its own.

routes.decode is that question, once, beside the unwrap that answers the
envelope one: the wire decision, then the envelope decision, each with a
name. A test walks the syntax tree and fails if any module decodes a body
on its own.

141 pass across the package and the two consumers that build on it.
2026-08-23 12:57:04 -07:00
zeekay 28035b6721 hanzo-iam: one client, one env reader, one verifier
The credential rode in the query string on AsyncIAMClient too — the
exported one — across seventeen call sites. Same fix as the sync client:
Authorization, Basic for a confidential client.

AsyncIAMClient was DEFINED TWICE, in client.py and async_client.py, and
the copies had drifted to nineteen methods against thirty-eight. __init__
exports the one in async_client.py, so the other was unreachable; it is
gone.

Three readers of IAM_* disagreed about the tenant: IAMConfig.from_env and
fastapi.configure both defaulted the org to the literal "hanzo", so a Lux
or Zoo process that forgot IAM_ORG did not fail — it addressed the hanzo
tenant and reported hanzo's users as its own. Now one reader,
IAMConfig.from_env(org), which seeds the endpoint and the tenant from the
org it is given; organization has no default at all. A test walks the
package and asserts config.py is the only module that reads the
environment.

IAMClient.validate_token and validate_token_with_cert were a second,
weaker verifier — no issuer check, no require:["exp"]. tokens.verify does
both and fails closed, so the strict one is what survives.

118 pass, and the seventeen tests of the two consumers that build on it.
2026-08-23 12:51:30 -07:00
zeekay aa18787d58 hanzo iam: address the routes IAM serves
Every command in this tree called /api/get-users, /api/set-password,
/api/add-provider and twenty-five more — the Casdoor-era surface. IAM
serves /v1/iam/<entity> and /v1/iam/password; all twenty-eight 404d. The
client id and secret rode in the query string besides, where the server
never looks and every log does.

IAM's entities all have the same five routes, so this is one
implementation of them over a table of fourteen entities rather than
fourteen hand-written command trees: 1391 lines to 300, and every entity
gains the verbs only some of them had. The credential goes in
Authorization — Basic for a confidential client, a bearer when one is
exported — and configure writes ~/.hanzo/auth.json 0600.

test_iam_cli reads IAM's own route registrations out of its Go source and
asserts every route this CLI names is one of them: 70 checked, and it is
what caught certs/get answering GET where the table said POST.
2026-08-23 12:46:01 -07:00
zeekay f526911603 hanzo-memory: the vector store is local, not a tracked artifact
343MB over 8,197 files of a developer's own LanceDB — chat_sessions,
memories_test_user_123, facts_test_kb, and embedded markdown — rode in
every clone of a public repo. Untracked and gitignored; the directory
stays where it is on disk.
2026-08-23 12:34:47 -07:00
zeekay 02f2ec2014 hanzo-mcp: the README documents the server that exists
The quickstart called create_mcp_server, server.register_*_tools and
server.tools.read — no such names. The real surface is the hanzo-mcp
command, its flags, and HanzoMCPServer; the tools come from whichever
hanzo-tools-* packages are installed, through the hanzo.tools entry
point. Verified against --version, --help and the constructor.

Drop the three *_COMPLETE.md status reports, MODULAR_ARCHITECTURE.md
(a proposal), COMPREHENSIVE_DOCUMENTATION.md (claims 70+ tools over a
package with seven) and SEARCH_GUIDE.md (documents an ffind tool that
does not exist).

SECURITY_PATCH.md named files and lines for three findings in 0.8.13;
all three are addressed in 0.15.15 — the shell escaper it flagged is
gone with its module, permissions.py refuses .. and ~ before resolving,
and the third asked for a token the server minted, persisted to
~/.hanzo/mcp_token and never once checked. That token is gone too: auth
is IAM's, and a credential nothing verifies reads as a gate that is not
there.
2026-08-23 12:31:08 -07:00
zeekay 5bc4b34731 iam: the credential rides in Authorization, not the query string
hanzo-iam's admin calls sent clientId/clientSecret as query parameters.
IAM's app() reads httpx.Basic(c) — the Authorization header and nothing
else (internal/authz/authz.go) — so the pair authenticated nothing and
landed in every access log, proxy log and Referer on the path.

_admin_headers now renders it as HTTP Basic (RFC 6749 2.3.1), a bearer
still wins where one is held, and _admin_params is gone rather than left
returning an empty dict. The async class gets the same one helper.

test_credential_location pins it on both arms: put the pair back in the
query and the test fails on the string it leaks.
2026-08-23 12:23:26 -07:00
zeekay 791a13a0ed Regenerate against the current API document
74 commits of cloud had landed since these clients were last cut: +7 operations,
-2. The additions include GetBaseBases and GetBaseBasesByOrg — the listing that
answers which Bases a caller can reach, which is why a workspace built on this
client could not show one.

.spec-lock moves with the code. It named the older document while the tree was
regenerated from the newer one, and a lock that disagrees with the files beside
it is worse than no lock: it is what the next reader checks to decide whether
they are behind.
2026-08-23 10:01:23 -07:00
zeekay 841130f02e Regenerate against the released document (v8.5.102)
Projected from hanzo-inc/cloud@59ec4deb4 openapi.yaml, sha256
e7e90d5445aeccfe332d942d0edd7a77df14336d97a445b71fb2f3047db65192.

By hand: the CI lane clones the private driver with a KMS-minted IAM machine
token, and that subject is linked to no account on git.hanzo.ai, so it verifies
and authorizes nothing.
2026-08-22 08:01:30 -07:00
zeekay be1e2814c4 name the neighbour, so the wrong one is not installed
hanzo-agent and hanzo-agents differ by one letter and are different products:
one is an SDK for building agents, this is a CLI for driving them. They share no
code and neither depends on the other, which is exactly why the names mislead.
Each README now says which is which.
2026-08-21 16:21:43 +00:00
zeekay a0ee31e20f name the neighbour in the docstring too
Same one-letter confusion as the README: hanzo-agent is the SDK, hanzo-agents is
this CLI. A reader who reaches the module before the README should still be told.
2026-08-21 16:21:42 +00:00
zeekay 9d7794d81c name the neighbour, so the wrong one is not installed
hanzo-agent and hanzo-agents differ by one letter and are different products:
one is an SDK for building agents, this is a CLI for driving them. They share no
code and neither depends on the other, which is exactly why the names mislead.
Each README now says which is which.
2026-08-21 16:21:30 +00:00
zeekayandHanzo Dev 2063e57090 the docs theme gets the icon it asks for
mkdocs.yml names assets/favicon.svg beside assets/logo.svg. Only the logo was
committed, so every build of the hanzo-agent docs resolved the favicon to
nothing and served the default.

The publish error also offered "a forge" as a place to put a PyPI token. It
means git.hanzo.ai; say that.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-20 19:53:51 -07:00
zeekayandHanzo Dev f06d33b3a9 spec: regenerate onto cloud@dd98c965
The committed client came from a document 262 commits old. Regenerated with the
lane own inputs -- scripts/generate.sh, the spec at a pinned ref, the driver
checkout -- so this is the derivation CI does, not a second one.

The surface moves the way the document moved: the datastore, docdb, kv and sql
clients collapse into instances (those routes re-homed under /v1/instances),
allowance arrives, sentry becomes sentinel, and the text-to-speech,
speech-to-text and health clients go because cloud no longer serves them.

No version bump. Publishing is a decision, and this commit is only about the
client being true.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-20 09:46:20 -07:00
zeekayandHanzo Dev 67304c3c12 the lock names the repo that holds the document
Every SDK locked its spec to hanzoai/cloud. That repo is a 12-commit stub with
no openapi.yaml in it at all, so the raw fetch answered 404 and the client lane
has failed on every push since. The document is in hanzo-inc/cloud — 10,618
commits, where the locked ref resolves and openapi.yaml at it hashes to exactly
the sha256 this lock already names.

Naming the real location does not by itself restore the lane: that repo is
private, and the hanzoai token reads it as not-found. But a lock pointing at a
repo with no document can never work, and this one works the moment the token
can see it.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-19 21:36:25 -07:00
zooqueen 95cf0799f9 the browser transport is the zapd consumer, and only that
hanzo_mcp.zap_server imported hanzo_tools.browser.zap_server, which does not
exist — the module could not be imported at all. Its caller swallowed that in
`except ImportError: return`, so _start_zap_server() ran on every `run()` and
started nothing, and the HANZO_NO_ZAP switch guarded a path that never had an
effect either way.

The browser tool reaches Chrome through the native host -> zapd -> the socket
at ~/.zap/run/zapd.sock, where hanzo-mcp is a consumer (hanzo_tools.browser
.zapd_consumer). That is the transport, so the WebSocket server and its mDNS
discovery go, along with _normalize_mcp_result, which existed to shape results
for that wire and had no other caller.
2026-08-18 23:13:53 -07:00
zeekayandHanzo Dev 22c0e467a3 cli: drop 'hanzo install runtime'
hanzoai/runtime is archived. The subcommand set up a checkout at
~/work/hanzo/runtime and installed the hanzo-runtime / @hanzo/runtime
SDKs — a source tree that no longer exists and packages that were never
really released (npm only ever had 0.0.0-dev; PyPI's last release was
2025-07-15).

Verified: hanzo.commands.install imports clean and install_group now
exposes 13 subcommands with 'runtime' absent.

(cherry picked from commit 16d31994dd)

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-15 09:07:02 -07:00
zeekay 80c31f3534 publish-pypi: read KMS at the route KMS serves
The job asked for /v1/kms/orgs/hanzo/secrets/<path>/<key> and parsed
.secret.value. KMS serves GET /v1/kms/secrets/<path>/<key>?env=<env> and
answers {"env","name","value"}, taking the org from the token's own claim,
so every read 404'd and every release reported the credential unseeded —
including hanzo/prod/python-sdk-publish/PYPI_TOKEN, which is there.

Root to 3.2.13, README install line names it. hanzo-research declares its
readme, license and authors, so its page carries them.
2026-08-14 17:42:58 -07:00
zeekay 4998a916fd the client sends the credential the document now declares
hanzoai/cloud@113cf7d6 declares components.securitySchemes.bearer and applies it
document-wide, so regenerating writes a populated Configuration.auth_settings()
and auth_settings=['bearer'] into 2498 call sites; ApiClient._apply_auth_params
sets Authorization: Bearer <token> from access_token, which was read by nothing
before. Four call sites carry an empty list — exactly the operations the document
marks security: [] (get_models, get_models_providers, get_commands,
get_openapi_json) — so the public routes still go out with no credential.

examples/client.py drops the header_name/header_value compensation and passes
access_token. examples/models is a new flow on the public catalog: it runs before
a reader has a key, which makes the install checkable in one command.

Proven on api.hanzo.ai, not by reading: examples.models prints 112 models with no
credential; examples.hello returns the caller's keys with a real sk- and 403
"sign in to manage API keys" with a bogus one.
2026-08-14 17:22:30 -07:00
zeekay b739f2ab6e hanzo-network: carry exo's GPL-3.0 and attribute the fork
The distributed inference, topology and device-capability code derives
from exo (github.com/exo-explore/exo), which was GPL-3.0 when this fork
was taken. The package declared BSD-3-Clause, which grants terms the
upstream licence does not.

Ship exo's GPL-3.0 text, add a NOTICE naming upstream and recording the
modifications, and declare GPL-3.0-only. hanzo-network stays an optional
extra with guarded imports, so the base CLI is unaffected.
2026-08-14 16:55:05 -07:00
zeekay bcbea8ce31 drop a script that existed to hold a key
openai_api_check.py was not a test — pytest never collected it, nothing imported
it, and its whole body was a hand-run connectivity probe against a vendor with
the credential written in as a literal. There is nothing to keep.

The value it carried must still be rotated: it is in this repo's history and the
history is public.
2026-08-14 16:52:07 -07:00
zeekay ef4f774da5 the credential comes from the environment
This script carried its key as a literal. A key in a source file is a key in
every clone of that file, and this repo mirrors publicly, so the value must be
rotated independently of this change — removing it from the tip does not remove
it from history.

Reads OPENROUTER_API_KEY and exits with a clear message when it is unset.
2026-08-14 16:51:14 -07:00
zooqueen 7166ca70bf chat: point at the two commands that actually start a local endpoint
`hanzo router start` and `hanzo serve` are neither of them commands. The cloud
comes up with `hanzo up`; one model with `hanzo engine serve`.
2026-08-14 16:30:13 -07:00
zeekay 7a5968f9dc a pk- key authenticates nobody, so stop telling readers it reads
The README offered the two key shapes as a permission split — pk- to read,
sk- to write. cloud's own identity boundary says otherwise: middleware_identity
returns nil for any pk- before it consults the key store, because a key meant
for a browser bundle must not read. A pk- resolves to an org so a beacon can be
attributed to a tenant, and that is all it does.

Following the old sentence, a reader exports a pk- and every one of the five
flows answers 403 — /v1/tools spells out why, "a validated principal is
required" — and that refusal reads like a revoked key rather than the wrong
shape of key. examples/client.py now refuses a pk- before the request goes out,
the same argument that already rejects an unsendable access_token.

Also: three status codes in example docstrings had drifted from what the API
answers now (/v1/kv/keys/* 404s on every verb, /v1/automations/mcp 404s), and
two dead files went with them — examples/.keep, whose directory has held real
flows for a while, and scripts/mock, which reads a .stats.yml this repo does
not have and which scripts/test already says is unnecessary.
2026-08-14 14:45:57 -07:00
zeekay 241ca6dc59 the front door tells the truth about the client behind it
README: PyPI serves 3.2.1 and this tree is 3.2.11, so the install line is the
git one. Between the two the default version left the operation ids — 3.2.1
carries get_v1_keys/get_v1_tools (182 api modules, 2172 models) and this carries
get_keys/get_tools — so `pip install hanzoai` above a `get_keys()` quickstart
hands the reader a client that cannot run it. Counts are measured off the pinned
document: 1814 paths, 2479 operations, 191 tags + default_api = 192 API classes
over 2460 models, 2659 importable modules.

Auth was wrong everywhere and it was the same root cause: the document declares
no securityScheme, so every operation got an empty auth_settings and
Configuration(access_token=…) is read by nothing. Serializing get_keys from such
a Configuration produces a request with no Authorization header at all — the
five flows have been calling anonymously and reading the refusal as a bad key.
examples/client.py now passes the bearer through ApiClient's header_name/value
pair, verified against a local server that echoes what it received, and a smoke
test pins both halves so the day cloud emits the scheme, the test fails and the
header moves back into Configuration.

The examples gate resolved class names and stopped there, which is how `money`
and `tools` once passed it while every call in them named a method that no
longer existed. It now walks the syntax tree for attribute accesses on anything
bound to a *Api and asks the class: ten names resolve, and a stale get_v1_tools
fails in the same second the import passes.

Deleted five example scripts that imported hanzoai.Hanzo, hanzoai.cluster,
hanzoai.agents and hanzoai.completion — none of which exist — or were dict
literals captioned "in practice this would be called through the MCP interface";
compileall passed all five, because syntax is not a claim. Deleted three docs
that were fabrication rather than drift and that nothing rendered: FEATURES.md,
QUICKSTART.md, GPT5_ORCHESTRATION.md.

pkg/hanzo-agent said `pip install hanzoai` and `from hanzoai import Agent,
Runner`; the client has no Agent. Its own `full` extra resolved to
hanzoai[web3,tee,marketplace,cli], extras the client does not declare, so it
installed the client and none of the extensions. pyproject said BSD-3-Clause
while LICENSE is the Apache 2.0 text. test_smoke imported tracker_api and no
/v1/tracker path is emitted any more, so the suite carried a failing test naming
a module the client does not have; it reads analytics_api now, off the client.
2026-08-14 14:31:08 -07:00
zeekay d1fa0676d1 3.2.10 — the client says what the wire says
Regenerated from hanzoai/cloud@6a719cdc (.spec-lock carries the ref and the
digest). 3.2.9 was a projection of a document 343 commits old, and in between
the operationId stopped carrying the default version: 5196 method names the
client published — `get_v1_tools`, `post_v1_agent`, every one of them — name
operations the document no longer declares. None remain.

The id is one identity, not four. An operationId is the SDK method, the MCP
tool name, the CLI spelling and the by-name key, so a client that keeps the
old spelling does not merely look stale — it teaches a name nothing answers to.

Shape: 1813 paths / 2479 operations -> 192 api modules + 2460 model modules,
all 2659 importable.
2026-08-14 11:01:13 -07:00
zeekayandHanzo Dev 1fe73de0c3 hanzo-sandbox: a client for the sandboxes cloud actually serves
The package is named for the cloud app it speaks to — cloud apps/sandbox is
hanzo-sandbox here — so there is one name per concept across the two languages.

Built against the routes rather than guessed:

    GET|POST   /v1/sandboxes           list, create
    GET|DELETE /v1/sandboxes/:id       get, delete
    POST       /v1/sandboxes/:id/exec  argv | command, stdin, dir, timeoutSec
    GET|POST   /v1/sandboxes/:id/fs    read, write

The API key carries the org, and the client sends nothing else. cloud resolves
it through principal.OrgOf(user, org), which returns nothing without a validated
principal — "no validated principal, so the org claim is untrusted" — and the
gateway strips a client-supplied X-Org-Id before a handler sees it. A first cut
took `org` as a required argument and sent the header on every call; that would
have been ignored at best.

Two things the tests found, both real:

  * with no key configured the client still sent `Authorization: Bearer `, an
    empty credential that reads as a present one and hides "no key" behind
    whatever the server says about a bad one. It sends no header now.
  * cloud spells the size field `class`, which python cannot, so Sandbox maps it
    to `cls` at the boundary instead of leaving callers to index a dict.

This is the REMOTE half. hanzo_sandbox.sandbox stays the local one — unshare,
namespaces, a boundary on this machine. Same word, unrelated concerns, separate
modules.

Served by a real HTTP server in the tests, since what breaks in a client of this
shape is the request: path, method, credential.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 19:36:57 -07:00