A step whose curls are expected to fail cannot run under -e, and a local rehearsal with plain `bash script.sh` will not show it. Run 69709 vs 69723.
9.7 KiB
java-sdk — notes for the next person in here
One Gradle module, hanzo-java-cloud, published as ai.hanzo:hanzo-java-cloud.
examples/ is a second module that exists only to be compiled against it.
What is generated and what is not
hanzo-java-cloud/src/main/java/ai/hanzo/cloud/** is generated and is deleted
and rewritten wholesale on every regeneration — sdks.yaml's take: hands
the generator that directory outright. Never edit a file in it; a hand edit is
caught by generate.py --check and fails CI.
Everything else is the repo's, including ai/hanzo/Hanzo.java, which sits
beside the generated package for exactly that reason.
The shape, and how to measure it
find hanzo-java-cloud/src/main/java/ai/hanzo/cloud -name '*.java' | wc -l # 2670
ls hanzo-java-cloud/src/main/java/ai/hanzo/cloud/api/*.java | wc -l # 192
ls hanzo-java-cloud/src/main/java/ai/hanzo/cloud/model/*.java | wc -l # 2461
grep -rhoE 'public okhttp3\.Call [a-zA-Z0-9_]+Call\(' \
hanzo-java-cloud/src/main/java/ai/hanzo/cloud/api/*.java | wc -l # 2502
The document (hanzoai/cloud openapi.yaml, pinned in .spec-lock) declares
1,814 paths, 2,479 operations, 190 tags, 2,436 schemas, and its operation
ids are identical to what api.hanzo.ai/v1/openapi.json serves.
2,479 operations become 2,502 methods over 192 classes because 23 operations
carry two tags and are emitted into both, and 50 carry none and land in
DefaultApi (the /, /.well-known/*, /_/* and git-transport routes).
A method name is its operation id camel-cased: get_kv_by_name → getKvByName.
The credential is generated now
The document declares a bearer scheme (HTTP bearer) and applies it to every
operation except four. So ApiClient registers an HttpBearerAuth, and:
grep -rc 'localVarAuthNames = new String\[\] { "bearer" }' \
hanzo-java-cloud/src/main/java/ai/hanzo/cloud/api/*.java | paste -sd+ | bc # 2498
grep -rc 'localVarAuthNames = new String\[\] { }' \
hanzo-java-cloud/src/main/java/ai/hanzo/cloud/api/*.java | paste -sd+ | bc # 4
The four are /v1/commands, /v1/models, /v1/models/providers and
/v1/openapi.json — exactly the operations carrying security: [], so the
client refuses to send a credential where the document says none is wanted.
ai.hanzo.Hanzo did that job by hand until the scheme existed, setting
Authorization as a default header. It no longer does: it calls
setBearerToken, so there is one code path for the credential and it is the
generated one. What remains hand-written is the part no scheme covers — reading
the environment, the default base URL, and X-Org-Id, which selects a tenant
rather than authenticating one.
Dependencies are what the sources import, nothing more
jakarta.annotation-api is on the 1.x line and must stay there: every
generated type carries @javax.annotation.Generated, and the javax namespace
only exists in 1.x — 2.0 renames it to jakarta.annotation and 2,470 files stop
compiling. jsr305 supplies @Nullable/@Nonnull and does not supply
Generated. Dropping either is a 2,600-error build.
Nothing is published, and what it waits on
repo1.maven.org/maven2/ai/hanzo/ is a 404, Central's index has no ai.hanzo
group, and a full-text search of Central for hanzo returns 0 of its 21.3M
components. Until an upload succeeds the README's install path is
publishToMavenLocal — do not print coordinates that resolve nowhere.
Nor is any number claimed: git ls-remote --tags origin returns 0. The
v0.1.0-alpha.{3,4,5} tags exist only in a local checkout and were never
pushed, so they claim nothing. A version is claimed by a tag on the remote —
read that, not the manifest, before choosing the next one.
The lane is ready and the artifact is consumable; the credential does not exist. Tagging today gets a run that stops at the first step and names what is missing. Two things are wanted, and neither is this repo's to create:
- A Central Portal user token, as
MAVEN_CENTRAL_USERNAMEandMAVEN_CENTRAL_PASSWORDin KMS (orghanzo, envprod), plus an ASCII-armoured PGP key asMAVEN_GPG_PRIVATE_KEY/MAVEN_GPG_PASSPHRASE. All four read 404 there. KMS holdsNPM_TOKENandPYPI_TOKENand no Maven equivalent, in any of its environments; the forge'shanzoai,hanzo-incandhanzo-appsorg secrets hold none either, and this repo has no secrets of its own on either host. Nothing in the estate names them except this repo. - The
ai.hanzonamespace, verified. Sonatype checks a TXT record on the exact reversed domain —hanzo.aiforai.hanzo— andhanzo.aipublishes only a Google site-verification and an SPF record. Verification is per account, so it cannot be confirmed from outside without the token from (1).
Both are one-time acts by whoever holds the hanzo.ai DNS zone and can open the Central account. After that, a tag publishes with no further change here.
Three traps this lane used to contain
An unsigned upload used to report success. signing was keyed on whether
MAVEN_GPG_PRIVATE_KEY was set, so with no key sign was never called, the
publish graph held no signing task at all, and the jars would have gone out
unsigned with BUILD SUCCESSFUL — Central refusing them hours later, out of
band. The signature now follows the destination
(required = { gradle.taskGraph.hasTask(':hanzo-java-cloud:publishMavenPublicationToCentralRepository') }),
so a keyless Central publish dies at signMavenPublication before a byte moves,
and publishToMavenLocal still needs no key.
Uploading is not releasing. Gradle's maven-publish is a series of PUTs
with nothing marking where one deployment ends, so Sonatype's OSSRH-compat
service separates them by source IP and waits to be told. Without a following
POST /manual/upload/defaultRepository/ai.hanzo?publishing_type=automatic the
artifacts sit in an open staging repository and never reach the Portal, let
alone Central — Sonatype's own words: "you must follow this section or your
deployments will not be visible in the Central Publisher Portal." That call is
the last step of release.yml, in the same job so it comes from the same IP.
A step that measures failure cannot run under -e. The forge invokes every
run: as bash --noprofile --norc -e -o pipefail, and set -uo pipefail
inside the script does not turn -e back off. The KMS step exists to find out
whether four secrets are there, so its curls are expected to 404 — under -e
the first one took the step at curl's exit 22 and none of its messages were ever
reached. Run 69709 reported a missing credential as exitcode '22'; run 69723,
after set +e, reports exitcode '1' and names MAVEN_CENTRAL_USERNAME. A
local rehearsal with plain bash script.sh cannot catch this — rehearse with
bash -e -o pipefail script.sh. hanzoai/ci's build.yml carries the same
warning on its own KMS step.
What is already proven
Everything except the two acts above. ai.hanzo:hanzo-java-cloud:8.0.1 built,
installed to ~/.m2, and resolved from a separate project with an empty
GRADLE_USER_HOME — the full transitive tree (okhttp, gson, okio, kotlin-stdlib)
came down from Central against the generated POM. The README quickstart, compiled
against that resolved jar, printed GET /v1/keys answered; 0 key(s) owned with
an IAM access token and 403 {"code":"forbidden"} without one. The POM carries
every coordinate Central validates: name, description, url, licence, developer,
scm — and github.com/hanzoai/java-sdk is public, so its urls resolve for a
reader. Signing was exercised with a throwaway key: five .asc files, and
gpg --verify returns Good signature.
Building
./gradlew --no-daemon :hanzo-java-cloud:compileJava # ~25s cold
./gradlew --no-daemon :examples:compileJava
Set GRADLE_USER_HOME and TMPDIR to somewhere with room. The compile is
forked with -Xmx6g (root build.gradle) because the Gradle daemon's heap is
not the compiler's, and 2,671 sources will not fit in the default.
The six examples are not this repo's choice
They are data in hanzoai/openapi flows.yaml — a closed set every Hanzo SDK
owes — and test_flows.py there asserts both that the set is exactly those six
and that every operation id they name exists in the document. Adding a seventh
here breaks that.
chat and money print a status rather than a body: those routes are published
with no request or response schema, so the generated methods take no argument
and return void. Do not hand-roll a request to make them look better — that is
the drift these SDKs exist to prevent. When cloud declares the shapes, a
regeneration fixes them.
Running one, and which credential answers
Compiling proves the client matches the document; only running proves it can
authenticate. HANZO_API_KEY=… ./gradlew --console=plain -q :examples:hello,
and unset it for the control — the pair is the proof.
Two kinds of token reach api.hanzo.ai and they do NOT behave alike:
- An IAM access token —
POST /v1/iam/oauth/token,grant_type=client_credentials, with a client id and secret (services keep theirs in the k8s Secret<app>-iam-creds). Measured:/v1/keys200 ten times out of ten. - A Cloud API key (
sk-). Measured on the same route in the same minute: 200 once in ten, 403 the other nine, while/v1/toolsand/v1/billing/balanceanswered 200 every time. The key is fine; the key→account lookup resolves on one replica and not the rest, and/v1/billing/balancemakes that visible by answering 200 with"account":"anonymous". A single 403 fromhellois therefore not evidence about the client. Repeat it, or use an IAM token, before concluding anything about this repo.