The relay gate and the money bits asked authz's PlatformSudo, which granted platform authority to an admin membership held at any position, so a brand org's member added to admin got X-User-IsAdmin and the Admin bit here while cloud and IAM said no. authz v1.10.47 has no PlatformSudo; both transports now ask Claims.Sudo. cmd/admin-api asked the owner CLAIM, the org of the application that minted the token, behind an IAM_ADMIN_ORG knob. A bearer is now asked Sudo, and a session the server's isSuperAdmin verdict. The knob and the row-level is_global_admin column, a copy of the predicate nothing read, are gone. authz v1.10.47 brings zip v1.37.3, whose untyped routes are Raw, and brand orgs that open only to the people who run them, so the fixtures' plain member now lives in a customer org.
Hanzo Gateway
Gateway is the trust boundary in front of the Hanzo API. Every request to
api.hanzo.ai passes through it, and what it does is decide who the caller is
and whether the request may proceed — validate the JWT against Hanzo IAM's
JWKS, throw away every identity header the client sent, write the canonical
identity headers from the verified token, apply rate limits and circuit
breakers, emit telemetry, and hand the request on.
It is not a router, and it deliberately does not have a route map. For
api.hanzo.ai it forwards /* straight to hanzoai/cloud
with no per-route allow-list. Cloud's mount table is the one routing source of
truth; a second table here would be a second thing to get out of sync. The
comment above apiCloudHosts in routes.go says it plainly, and
that is the rule to preserve when changing this repo.
The value of the boundary is exactly that downstream services never have to
wonder whether X-Org-Id came from a token or from a curl flag. It came from a
token, because gateway strips it unconditionally on the way in and rewrites it
on the way out.
Where it sits
Internet
-> Cloudflare
-> hanzoai/ingress TLS termination, L7, the front door
-> hanzoai/gateway identity, rate limit, circuit break, telemetry
-> hanzoai/cloud owns /v1/* routing (HIP-0106 mount table)
Ingress and gateway are different jobs and both are needed. Ingress terminates TLS and decides which cluster service a hostname belongs to. Gateway decides who the caller is. Neither does the other's work.
Implements HIP-0026 (the identity-header contract) and HIP-0106 (unified cloud
binary; gateway exposes Mount() so it can run in-process inside the cloud
binary instead of as a separate hop).
Identity headers
Stripped from every inbound request, unconditionally, before anything else reads them:
X-User-Id, X-Org-Id, X-Roles, X-User-Email, X-Phone-Number,
X-User-IsAdmin, X-User-Role, X-User-Roles, X-User-Name, X-Tenant-Id,
X-Org, and every X-IAM-* and X-HANZO-* variant.
Written from the validated JWT. These three are canonical:
| Header | Source claim |
|---|---|
X-User-Id |
sub |
X-Org-Id |
owner |
X-Roles |
roles, comma-joined |
Plus X-User-Email (email), X-Phone-Number (phone_number / phone) and
X-User-IsAdmin (isAdmin) when the token carries them.
Authorization, Content-Type, Accept and X-Request-ID pass through
untouched. Opaque API keys (sk-, pk-, fw_, hz_) are not JWTs and
are forwarded to the backend that owns them for validation.
Calling the API
Get a key from console.hanzo.ai and send it as a bearer token:
curl https://api.hanzo.ai/v1/chat/completions \
-H "Authorization: Bearer $HANZO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"enso","messages":[{"role":"user","content":"Hello"}]}'
Model ids come from the catalog, not from this repo — gateway does not know
what a model is. curl https://catalog.hanzo.ai/v1/models lists them.
The API surface itself is documented at
docs.hanzo.ai/docs; the route table lives in
hanzoai/cloud. This README deliberately does not reproduce either, because a
copy here would be the second source of truth the design exists to avoid.
Rate limiting
Configured in the mounted gateway.json under qos/ratelimit/router.
Values in configs/example/gateway.json:
| Scope | Limit |
|---|---|
| Global | 5,000 req/s |
| Per client IP | 100 req/s |
Individual endpoints can override this in their own extra_config. Rejected
requests get 429 with Retry-After.
Health
| Path | What it answers |
|---|---|
/healthz |
The process is up |
/health |
Backends are reachable |
Build and run
make build # gateway binary (legacy HTTP edge, BUILD_TAGS=legacy)
make test # both builds
make validate # validate every config
Images are published per brand, not as a bare latest:
docker run -p 8080:8080 ghcr.io/hanzoai/gateway:hanzo-latest
hanzo-latest and lux-latest are the two brand tags, alongside
<brand>-<sha> for pinning. make docker-hanzo / make docker-lux build
them; make deploy-hanzo / make deploy-lux apply to the clusters.
GOEXPERIMENT=jsonv2
The HIP-0106 in-process mount runs on hanzoai/zip, which routes JSON through
stdlib encoding/json/v2 when the binary is built with the experiment on. The
Dockerfile and CI set it; manual builds should too:
GOEXPERIMENT=jsonv2 make build
Without it the binary still builds and runs — zip falls back to encoding/json
v1. v2 is preferred in production: roughly 10% faster and 25% fewer allocations
per request. The startup log line json_variant=encoding/json/v2 confirms it is
active. No third-party JSON library is permitted in the Hanzo Go stack; stdlib
only.
Configuration
configs/
example/gateway.json local example; a deployment mounts its own at
/etc/gateway/gateway.json (the image carries none)
lux/ Lux brand
k8s/ cluster manifests
cmd/gateway/ gateway entry point
tests/ integration tests
An endpoint entry declares the path, the input headers it is allowed to carry,
and the backend host — no path rewriting, since cloud speaks /v1/* natively:
{
"endpoint": "/v1/chat/completions",
"method": "POST",
"input_headers": ["Authorization", "Content-Type", "Accept", "X-Request-ID",
"X-Org-Id", "X-Project-Id", "X-Billing-Account-Id"],
"output_encoding": "no-op",
"backend": [{
"url_pattern": "/v1/chat/completions",
"host": ["http://cloud.hanzo.svc.cluster.local:8000"],
"encoding": "no-op"
}]
}
These per-endpoint entries drive the legacy HTTP edge build. The default build
is the passthrough relay described at the top: it does not consult them for
api.hanzo.ai.
Run make validate after any config edit.
Related
| Repository | Role |
|---|---|
hanzoai/ingress |
The L7 front door — TLS, Kubernetes Ingress, load balancing |
hanzoai/cloud |
The API itself; owns the /v1/* mount table |
hanzoai/iam |
Issues the tokens gateway validates |
hanzoai/engine |
Serves models on cloud GPUs |
hanzoai/edge |
On-device inference runtime — runs models locally, not part of this path |
License
Apache-2.0 — see LICENSE and NOTICE.
The service-assembly layer derives from upstream Apache-2.0 projects and is built on the Lura framework. The authoritative attributions are preserved verbatim in NOTICE.