The projection was pinned to a commit that exists only on this machine, so
the receipt named a document no consumer could resolve. The document itself
is byte-identical at both commits -- not one generated line moves -- which is
why this is a one-line correction and not a regeneration.
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.
The filenames read Zen-*.woff2 and the glyphs are right; only the name
table said family='Geist', designer='Basement.studio, Andres Briganti,
Mateo Zaragoza', vendorURL=basement.studio. Nothing visual can see that —
a font picker, a local() match and a licence audit can.
Replaced with the corrected bytes from hanzoai/font, matched by glyph
content rather than by filename, so the drawing is provably the same face.
.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]>
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]>
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]>
`hello` called KeysApi.get_keys(). There is no /v1/keys — API keys are one noun
at /v1/account/keys, so the call is AccountApi.get_account_keys().
`store` called KvApi.post_kv/get_kv_by_name/delete_kv_by_name. /v1/kv is the KV
DATA plane now, addressed by bucket and key; provisioning a KV instance — which
is what this example does — is /v1/provisioning/kv. Same ProvisionRequest, same
three steps, so it is a rename rather than a rewrite.
Both were caught by the examples gate and neither had been true for days, which
is why this repo's pipeline has been red since 2026-08-21 and no regenerated
client has published since.
examples.models is untouched: /v1/models is published and answers 200, and its
operationId is get_models, so ModelsApi appears once the client is regenerated —
which the pipeline does before it tests.
Co-authored-by: Hanzo Dev <[email protected]>
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.
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.
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.
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.
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.
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.
`pip install hanzoai==3.2.13` — PyPI serves 8.5.89. Anyone following the first
command on the page pinned themselves to a client from before most of the API
existed, and a pinned version in prose is stale the day after the next release.
It names no version now.
The note beneath it explaining that 3.2.1 and earlier used `get_v1_keys` rather
than `get_keys` goes with it: every published version is long past that rename,
so it described a client nobody can install by accident.
It opened by telling a developer the client covers so many operations over so
many paths in so many services, and repeated variations of that further down. The
number is not something a reader can act on, it was wrong — the document had
moved on — and keeping it right means measuring the document on every build to
maintain a sentence nobody needed.
The prose says what the client IS. Where a count explained real behaviour — some
operations declare no response body, so their method hands back nothing to
decode — the explanation stays and the tally goes.
It opened with "2,479 operations over 1,814 paths, grouped into 192 services".
The document serves 2,259 over 1,620 in 117 — every number wrong, in the first
paragraph a developer reads. They were true once and the document moved.
The count is now stamped by the generator from the spec each build was cut from,
inside a `<!--counts-->` span, so it cannot drift again. Any derived figures
beside it were re-measured against the same document rather than carried over.
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.
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.
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.
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.
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.
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]>
This repo pinned an exact hanzoai/ci release, so it kept running the build.yml
that reads git.hanzo.ai through HANZO_GIT_TOKEN — a credential no org seals.
The gate has been red on that ever since, while the sixty-four repos on @v1
moved past it.
@v1 is not an unproven tip. hanzoai/ci pins its own gate to the last release and
moves that pin first, so a release proves itself there before @v1 follows it.
An exact pin here opts out of that and rots instead.
Co-authored-by: Hanzo Dev <[email protected]>
LLM.md is the source and stays the source; CLAUDE.md is a symlink to it, so there is
one file and it cannot drift from itself. What changes is that the link is committed
rather than left for each checkout to recreate.
Left untracked, the doc was conditional on a step outside the repo: a fresh clone had
no CLAUDE.md, nothing loaded LLM.md, and an agent worked here having never read what
this repo already explains. Most of the estate already tracks theirs; this is the rest
agreeing with them.
Hanzo 8 is one product on one number. The API, the six generated clients, the
CLI and the console all answer to 8.5.89, so a client version names the API it
was cut from without a compatibility table.
8.5.89 is the first cut past the highest 8.x anyone had reached (@hanzo/console
8.5.88). Every register moves FORWARD into it; nothing is renumbered backwards.
Co-authored-by: Hanzo Dev <[email protected]>
The ref said which document; nothing said which version it is. A projection of
one document answers to that document's number, so the poke carries it and the
cut uses it verbatim instead of counting this repo's own patches.
Co-authored-by: Hanzo Dev <[email protected]>
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]>
v1.0.75 read the document with github.token, the per-job token, which is scoped
to the repo running the job and answers 404 for a private repo in another org.
Both reads in that lane cross one. The 404 read as a missing document rather
than a refused credential.
v1.0.76 gives those reads their own KMS-sealed HANZO_GIT_TOKEN and says so by
name when it is absent, so the next failure names its own remedy.
Co-authored-by: Hanzo Dev <[email protected]>
hanzoai/ci defaults client.spec.repo to hanzoai/cloud, and that repo is now the
re-rooted OSS core -- a short line sharing no ancestor with the product. The
ref this projection is pinned to does not exist there, so every run of the
client lane, check or move, died fetching it: `curl: (22) ... 404`.
Measured: the same token reads openapi.yaml at 113cf7d6 from hanzo-inc/cloud
with a 200 and from hanzoai/cloud with a 404. Nothing about the credential.
`.spec-lock`'s own repo= line does not set this -- ci reads the repo from
hanzo.yml and uses the lock's copy for a commit message -- so the two could
disagree in silence, and did. They are one fact and now agree.
Co-authored-by: Hanzo Dev <[email protected]>
This file claimed a bare dispatch took cloud's newest tag. ci v1.0.54 removed
that path, so what a poke really did was re-read the LOCKED ref, assert its
digest and exit before any write -- a check wearing a regeneration's clothes.
Measured: this projection sat 254 commits behind cloud while PyPI matched its
newest tag exactly, because publishing was never the broken half.
The ref now travels with the poke, as ci v1.0.75's spec-ref/spec-sha256 inputs.
That is the same fact repository_dispatch carried as client_payload.sha, spelled
for the one dispatch verb Hanzo Git serves. Omit both and the lane is a check
that writes nothing, exactly as today.
ci moves v1.0.54 -> v1.0.75 because the input does not exist before it.
Co-authored-by: Hanzo Dev <[email protected]>
"Forge" named nothing in this estate. The product is Hanzo Git, the address is
git.hanzo.ai, and the in-cluster service is hanzo-git.hanzo.svc. A generic word
invented for it sends a reader looking for a system that does not exist.
Comments only. ("unforgeable" stays — different word.)
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]>
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.
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]>
.generated names every path generate.py last wrote here. CI clones fresh, so a
manifest that exists only on the machine that ran the generator leaves the
driver unable to tell what it owns — and a file that stops being generated
stops being removed.
The four other clients already track it; this one did not.
RELEASE.md described GitHub Actions, GitHub secrets and three bin/ scripts
that each took a PyPI token off the environment. None of that publishes this
repo — .hanzo/workflows/publish-pypi.yml does, reading the token from KMS —
so the scripts are gone and the page describes the lane, its tag mapping, and
what to check before a tag makes a version permanent.
LLM.md carries the published state: hanzoai 3.2.13 with get_keys/get_tools,
and the KMS route that a 404 will otherwise misreport as a missing secret.
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.
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.
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.
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.