Commit Graph
809 Commits
Author SHA1 Message Date
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
zooqueenandzeekay a9330efb5c one credential, one subject: the scope rides the key, not the argument
An operator holds one key and serves N customers. Until now every call it made
on a customer's behalf was indistinguishable from every other, and nothing in
the SDK could tell them apart.

`Client.as_(subject)` binds a client to one of them. IAM mints a short-lived
token for that subject — POST hanzo.id/v1/iam/tokens/issue?id=…, on IAM's own
host, the subject in the query, the grant read off the key — and the scoped
client sends it on every call, holds it until it nears expiry, and re-mints once
on a 401. No operation takes a user id, so there is none to pass wrongly.

The other half is the answer a policy holds for a human. It arrives as 202, and
202 is a 2xx: a caller who only checks for a raise reads a queued call as one
that completed and returned nothing, because no operation declares a schema for
it. `Client` raises `Held` instead, carrying the approval that gates it, and
`result()` hands the same outcome back as a value whose two arms share no member
— an `Approval` has no `value`, a `Done` has no `id`. Discrimination is on the
body, not the status: the dozen long-running operations whose 202 means
"accepted" carry their own schema and pass straight through.

`Client` is the generated `ApiClient` plus those two behaviours, so all 196
generated API classes take it unchanged and there is one client, not two.

25 tests against a stub with `RESTClientObject`'s signature — the only thing the
mint and the client ever call — pin the mint URL and query, the camelCase
response, the cache and its skew, the re-mint on 401, and that a hold raises.

(cherry picked from commit cda66ab6a5e9f51d50e161bbb3dfe96648688fa2)
2026-09-03 23:39:08 -07:00
zeekay f91d612e2c Regenerate against the current cloud document 2026-09-03 16:15:52 -07:00
zeekay fae213a6c0 Follow the cloud surface through the git rename 2026-09-03 16:15:51 -07:00
zeekay 85ad519b16 every Hanzo dependency is the version npm serves
Ranges drift once nothing moves them: a caret that has not been rewritten
since it was written pins whatever was current that day, and an exact pin
cannot even take a patch. Both are updated here.

One line per package, so a surface reads the same components, the same
tokens and the same face as every other one.
2026-09-02 02:01:33 -07:00
zeekay f43d8ae283 every Hanzo dependency is the version npm serves
Ranges drift once nothing moves them: a caret that has not been rewritten
since it was written pins whatever was current that day, and an exact pin
cannot even take a patch. Both are updated here.

One line per package, so a surface reads the same components, the same
tokens and the same face as every other one.
2026-09-01 16:13:48 -07:00
zeekayandHanzo Dev 7c2cd20df4 Merge branch 'tasks/native-http-sdk' into main
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 10:16:41 -07:00
zeekayandHanzo Dev 1b0b40fe34 Merge branch 'fix/zap-read-what-luxd-emits'
Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 10:12:22 -07:00
zeekay 25c5297839 Read the model catalogue as the typed object it now is
GET /v1/models declares a response schema, so the generator emits
AiApi.get_models() -> AiModelList and no longer emits ModelsApi at all.
The example imported the class that folded and failed at import, which is
the whole examples gate.

It reads the typed object now instead of the raw body and its own JSON —
the change its docstring said would arrive the day the schema landed.
2026-09-01 10:11:09 -07:00
zeekayandHanzo Dev 39017ee7a1 hanzo-tasks: the client is the /v1/tasks contract, not a Temporal wrapper
The package dialed Temporal over gRPC, a transport hanzoai/tasks removed in
v3, so nothing it did could reach the engine. It is now the HTTP contract the
engine serves under /v1/tasks/namespaces/{ns}: start, signal-with-start, list,
describe, signal, query, cancel, terminate, reset, history, and schedules — one
method per route, IAM bearer, tested against the request shapes. A workflow's
body still runs in a Go or TypeScript worker.

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-01 07:58:30 -07:00
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
zeekayandHanzo Dev 95df0efbcf Read the wire version the network actually sends
luxfi/zap v1.2.6 — the version luxfi/node ships — emits wire version 2 by
default, and Go's Parse accepts 1 and 2 alike. This layer accepted only 1, so
every frame luxd sends was refused at the header.

The module header already says the codec constants come from zap.wire "so they
can never drift", and the canonical zap.wire had this right: VERSION1,
VERSION2, and a parse that admits both. The drift was here — the guard was
re-implemented locally against the single emitted VERSION instead of the
accepted pair. Source both constants from the canonical wire and admit what it
admits, so the claim in the header is true rather than aspirational.

Add the vectors that would have caught it. The suite round-tripped this layer
against itself; these are bytes captured from github.com/luxfi/zap v1.2.6 by
calling EncodeNodeIDHandshake, so they fail if this layer and the Go module
ever disagree. They also pin the accepted set exactly — 0 and 3 refused —
because admitting a frame the network refuses is as wrong as refusing one it
accepts, and pin that our handshake equals Go's in every byte but the version.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-28 23:38:41 -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 6b5e0734e7 One version per package, on the current release
Every dependency on a @hanzo/@luxfi/@zooai package names the same version across
the repo: a catalog entry where pnpm supports one, an exact pin where it does
not, and a range only where a peer genuinely means one.

pnpm itself is pinned too — the estate ran eighteen different versions, five of
them too old to read a catalog.
2026-08-28 15:53:27 -07:00
zeekay 074facbba5 Name Zen in the hero card's font stack
The social card asked for Inter first. That is not a face this estate
licenses, ships or ever drew, so a renderer with Inter installed painted
the wrong face and one without it silently fell through to system-ui.
Zen leads now; the same tail follows it.
2026-08-28 11:59:32 -07:00
zeekay 9c18306471 fix(type): the file named Zen contained Geist
The name table of Zen-Variable.woff2 read "Geist" and ZenMono-Variable.woff2
read "Geist Mono". The filename, the @font-face and the licence beside them all
said Zen; only the bytes disagreed, so every grep passed and the surface
rendered Vercel's face.

This is the fourth instance in the estate — @hanzo/design shipped the same thing
under the same filename, zoo's foundation declared Zen over inter-var-latin,
and hanzo.sh had Zen declared over a Geist file. A filename proves nothing; the
check is the name table:

  python3 -c "from fontTools.ttLib import TTFont; print(TTFont(P)[\"name\"].getDebugName(1))"

The bytes are @hanzo/font 1.9.2's now, verified that way: family Zen and Zen
Mono. LICENSE-Zen.txt beside them already carried both copyright holders, which
is what OFL §2 asks of the copy that travels.
2026-08-28 11:48:44 -07:00
zeekay 301f737b45 release 8.5.156
One product, one number: the API and every client cut from it answer to the
version cloud's release train mints, so a reader holding 8.5.156 can name the
release it came from. The clients had drifted to their own counters -- 8.5.89,
8.5.91, 0.1.0, 0.1.0-alpha.5 -- because the fanout that carries the number can
only run in check mode on this forge and never writes.
v8.5.156
2026-08-28 08:55:51 -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 5a0aa5ee79 name the ref the fleet names, and one that can be fetched
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.
2026-08-27 13:53:26 -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
zeekay c32cd7e962 fix(font): the vendored Zen faces named Geist in their name table
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.
2026-08-24 23:33:54 -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
zeekayandHanzo Dev a43b78eaaf point two examples at the addresses their APIs moved to
`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]>
2026-08-23 16:10:25 -07:00
zeekay 5ddeca6b5a docs: self-host Zen in place of Geist over the jsDelivr CDN 2026-08-23 15:13:57 -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