Files
js-sdk/hanzo.yml
zeekayandHanzo Dev d8b09a274d feat: budget, policy, audit, search, kb and graph, over the generated client
Every Hanzo SDK offers the same six capabilities in the same six words. Go and
Python hang them off a client that already existed; TypeScript had none, so it
gains one — `Client`, holding an IAM client id and secret and minting its own
access token (client_secret_basic, RFC 8707 `resource`, held until 60s before
expiry, re-minted once on a 401). It takes no bearer and reads no
HANZO_API_KEY: a credential the SDK is handed is one nobody rotates and one
that says nothing about who is calling.

A refused budget and a refused policy are ANSWERS. `Answer<T>` is a union
discriminated on a literal tag — ok{value} | denied{code, reason, product?,
cures[]} | held{id, clause, reason}, all three carrying the x-request-id that
joins a call to its audit row — so `a.value` does not typecheck until the union
is narrowed and a switch that forgets an arm does not compile. The mapping from
HTTP to arm lives in one function; no capability can grow a rule of its own.
Exceptions stay for outcomes with no decision in them.

The layer is hand-written because the routes it needs are the ones the document
states as an address and not as a shape: /v1/authz/check declares neither a
request body nor a response, /v1/billing/balance and /usage declare no
response. Those three are modelled from what the served handlers read and
write, measured. It sits in src/ beside the generated tree because .generated
is the driver's manifest of what it owns — a file it does not name is this
repo's own and a regeneration never touches it — and the package entry moves to
src/hanzo.ts, which re-exports the generated src/index.ts and adds the client
beside it. Capability types are namespaced (budget.Allowance, search.Hit,
audit.Event, graph.Fact) because thirteen of the contract's names already
belong to generated models.

policy.check sends {subject, verb, path}, which is what
hanzoai/[email protected] serve/use.go reads, not the {sub, obj, act} the route's
own openapi.Describe prose states. The caller still says (sub, act, obj). The
prose and the handler are one fact and should be made to agree upstream.

Gates: `npm run wire` runs the six against a scripted fetch, asserting the
exact request each sends and decoding a realistic answer including both of
cloud's 402 bodies, a 202 hold and a 403 that is not a refusal. examples/six
drives all six in one pass against the live API and is type-checked with the
rest. The agent and tools flows are repaired for the tag names the current
document singularized (AgentsApi -> AgentApi, getTools -> getTool).

Co-authored-by: Hanzo Dev <[email protected]>
2026-09-10 14:56:16 -07:00

74 lines
3.9 KiB
YAML

# Hanzo CI — the ONE way this repo is gated, read by hanzoai/ci.
#
# No `images:` and no `deploy:`: this is a client library. A consumer installs
# it from npm at a tag, so what CI owes is a gate, not an artifact. Publishing
# stays where it already is — .github/workflows/publish.yml, on a push to main.
# Two publish paths would be one too many.
test:
- name: test
# `npm ci`, not `npm install`: the lockfile is committed and a gate that
# silently resolves a different axios than the release does is not a gate.
#
# Then `npm test` — package.json's own statement of what passing means,
# rather than a second one written here that can disagree with it. Four
# steps, each proving what the others cannot:
#
# `build` is both halves (CJS and the bundler-facing ESM), the only thing
# that type-checks the whole generated tree.
#
# `smoke` LOADS the built package by name, through package.json exports —
# the entry point `npm i hanzoai` opens. build and examples both read source; a
# broken module graph or an entry point that does not resolve is a fact
# about the published artifact that neither of them can see.
#
# `wire` runs the six capabilities — budget, policy, audit, search, kb,
# graph — against a scripted fetch, asserting the exact request each one
# sends and decoding a realistic answer, `denied` and `held` included. They
# are hand-written HTTP over the generated client, so what has to be proved
# about them is what goes out and what is made of what comes back; nothing
# else in this gate calls them at all.
#
# `examples` compiles the eight canonical flows — models, hello, chat,
# money, store, agent, tools, six — against the client the way a consumer does, importing
# `from 'hanzoai'` and reading real fields off real response models. It has
# already earned its place: `choices[0].message.content` failed to compile
# here (TS2339: Property 'message' does not exist on type 'object') because
# the spec typed chat choices as a bare object. That became hanzoai/openapi's
# ChatChoice schema, which fixed the same hole in every other language at
# once. No unit test on generated code would have found it — only calling
# the client does.
run: |
set -e
npm ci --no-audit --no-fund
npm test
# THE CLIENT LANE — this repo is a PROJECTION of one API document at one version.
#
# hanzoai/cloud's release sends `repository_dispatch: spec-update` carrying
# (version, sha, spec_sha256); hanzoai/ci fetches openapi.yaml AT THAT SHA,
# REFUSES if the bytes hash to anything else, runs `generate:`, writes
# `.spec-lock` beside the code, and — only after the `test:` block below has
# passed over exactly those bytes — commits and cuts a patch.
#
# The input is hanzoai/cloud `openapi.yaml` — emitted from the code by each
# app's own router and gated on every cloud release — not hanzoai/openapi
# `hanzo.yaml`, which is hand-merged and fed by nothing. A document the routers
# write beats one a human curates: it cannot name an address the server does not
# serve. `.spec-lock` records which one this tree projects.
client:
# WHICH cloud, said out loud. hanzoai/ci defaults this to `hanzoai/cloud`, and
# that path now serves the re-rooted OSS core -- a twelve-commit line sharing no
# ancestor with the product, and carrying no openapi.yaml at all. So the fetch
# 404s for every ref this projection could name, on check and move alike, and
# has since the repoint. It reads as a credential fault and is not one.
#
# The product line is what this SDK is a projection of, so it is what is named.
# `.spec-lock`'s own repo= line does NOT set this -- ci reads the repo from here
# and uses the lock's copy for a commit message -- so the two can disagree in
# silence. They are one fact; keep them equal.
spec:
repo: hanzo-inc/cloud
generate: ./scripts/generate.sh
version: 'package.json:jq -r .version package.json'