mirror of
https://github.com/hanzoai/js-sdk.git
synced 2026-10-30 15:02:31 +00:00
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]>
74 lines
3.9 KiB
YAML
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'
|