HIP-138: Cloud — Unified Hanzo Binary. Status Final. Hanzo's own standard — read this before implementing against it.
This document was numbered HIP-0106. That number is the plugin contract's — HIP-0116 and the code both cite it that way — so this one took the next free number in its block.
This proposal defines cloud — a single Go binary that imports every Hanzo-native subsystem as a package and dispatches requests to them by HTTP path, subsystem flag, or per-deployment configuration. The same binary powers api.hanzo.ai, api.osage.cloud, api.lux.cloud, api.zoo.cloud, and every other white-label resold cloud surface. Brand, enabled subsystems, and tenant scope are deployment configuration; the binary is the same artifact across all.
Naming note (2026-05-19 rename): the unified binary lives at hanzoai/cloud (this repo); the former LLM-control-plane content that used to live at hanzoai/cloud was renamed to hanzoai/ai and is now mounted as the ai subsystem inside this binary. White-label fork target is hanzoai/cloud — customers fork this one repo to launch their own ecosystem.
Inter-subsystem contract: ZAP (the Hanzo native binary protocol). Every subsystem ships its public interface as a .zap schema; zapc generates Go/TS/Python/Rust bindings; cloud wires the in-process ZAP-typed Go interfaces when subsystems are co-resident, falls back to ZAP RPC over the wire when split. No .capnp files anywhere in Hanzo-authored code.
Transport boundary — ZAP only, day one. Every Hanzo-authored transport AND data type is ZAP. There is no gRPC and no protobuf anywhere in Hanzo-authored code — zero google.golang.org/grpc, zero google.golang.org/protobuf, zero .proto, zero .pb.go. The only historical gRPC was dead Temporal-porting codegen, removed in hanzoai/tasks#12. Where an external standard must be spoken at the interop edge (OpenTelemetry OTLP for trace/log ingest, OpAMP for agent management), the protobuf wire is produced by the zap tooling — pb2zap converts the external .proto to a .zap schema (compiled by zapc to ZAP-native Go types), and zap2pb marshals those ZAP structs back to protobuf wire bytes at the boundary. Hanzo code holds only ZAP types and calls the generated converter; protobuf lives exclusively inside the ~/work/zap converter tool, never in a Hanzo service. Transitive protobuf pulled by foundational dependencies (beego/authz/coraza/lego/otel, and even luxfi/metric/client) is a dependency-side follow-up, not Hanzo-authored code.
Today Hanzo ships ~11+ Go-native services as independent binaries: IAM, Base, KMS, Gateway, Ingress, Commerce, AI (LLM control plane subsystem), o11y, AMQP, MCP (Go path), DNS, VFS, MQ, Authz, Agents. Each gets its own Dockerfile, Helm chart, deploy lane, CI workflow, release cadence, and observability spine. For Hanzo this is acceptable — we operate the cluster. For white-label resell (lux.cloud, zoo.cloud, osage.cloud, and others) it's not: every customer needs the same constellation of services, isolated per-tenant, billed per-tenant, auth'd against unified IAM. Shipping 11+ binaries × N customers × 3 environments multiplies operational surface beyond what a small team should run.
The audit at ~/work/hanzo/AUDIT_2026_05_19.md confirms ~35 Go-native services structurally ready for inclusion, of which 3 already export func Mount(r, deps) — commerce, gateway, and vfs. These are the reference implementations.
The unified binary collapses this:
ghcr.io/hanzoai/cloud:vX.Y.Z, that containsevery Hanzo Go subsystem as compiled code.
(cloud --enable=iam,base,kms,commerce,gateway,ai).
X-Org-Id from JWT picksthe tenant's Base instance, KMS namespace, IAM application, and brand.
org gets its own per-tenant data file (data/{orgSlug}.db for the SQLite backend, equivalent path for the ZapDB backend) with a per-org HKDF-derived DEK from a master key in KMS. Replicate works for both backends — SQLite WAL streaming and ZapDB-native log shipping use the same age-encrypted GCS bucket sink. Already the pattern in ~/work/hanzo/iam and ~/work/hanzo/base; this HIP makes it the universal contract.
trusts JWTs from one shared iam.hanzo.id JWKS endpoint. Gateway strips client-supplied identity headers, mints validated ones from the JWT, forwards in-process.
serves both in-process direct calls and over-wire RPC. Bootnode and other subsystems already use this pattern via zapc (the Hanzo ZAP compiler, Rust impl at ~/work/hanzo/zap/zapc-rs/) today.
JSON crosses the system boundary exactly once per request. Inside, every inter-subsystem call is ZAP-typed Go values — direct method calls when co-resident, ZAP RPC when split. JSON re-marshalling between Hanzo subsystems is a defect.
External clients
└── HTTP + JSON (stdlib encoding/json/v2 via hanzoai/zip jsonenc)
↓
ingress (TLS termination, log, rate limit)
└── ingress hands the request to gateway with the JSON body intact
↓
gateway (JWT validation, identity-header mint, route)
└── gateway hands the request to the subsystem with X-Org-Id +
the validated JWT; the body is still JSON until the subsystem
handler parses it
↓
subsystem handler (e.g. commerce, ai, mcp)
└── parses JSON body via zip.Ctx.Bind() — encoding/json/v2 when
GOEXPERIMENT=jsonv2, encoding/json v1 otherwise
└── inter-subsystem calls via cloud.Deps clients — ZAP-typed Go
values, no JSON re-marshal (in-process: direct method calls;
split: ZAP RPC over :9653)
└── response back to zip.Ctx.JSON() — same jsonenc, same variant
↓
gateway → ingress → external client
handler boundary.** Inter-subsystem code paths use ZAP-typed Go values — cloud.Deps.IAM.VerifyJWT(...), cloud.Deps.Commerce.GetTenantConfig(...), etc. No subsystem marshals a struct, hands it to another subsystem, and watches it get unmarshaled.
encoding/json/v2 is the canonical JSON impl. When the binary is compiled with GOEXPERIMENT=jsonv2, every JSON path in hanzoai/zip (and therefore every Hanzo HTTP handler) routes through stdlib encoding/json/v2. Without the flag, zip falls back to encoding/json v1. No third-party JSON library (goccy/go-json, sonic, jsoniter, …) is permitted in the Hanzo Go stack. Stdlib only.
The same cloud.<Subsystem>Client interface that resolves to a direct Go call when co-resident resolves to a ZAP RPC client when split. The subsystem caller does not branch on the mode. Inter-subsystem RPC rides the existing :9653 ZAP listener.
hanzoai/zip internal/jsonenc package; selected at compile time by goexperiment.jsonv2 build tag. zip.JSONVariant is a constant exposing which impl is active (encoding/json/v2 or encoding/json). zip.New logs it at startup so operators can confirm the impl from production logs.
are config-driven per the existing brand-package contract — no hanzo.ai hardcodes in the wire-stack logic. The same binary serves api.hanzo.ai, api.osage.cloud, api.lux.cloud, etc.
Per HIP-0106 "Subsystem-to-subsystem calls", cloud.Deps exposes typed clients (one per subsystem). cloud.BuildDeps(cfg) picks the implementation per subsystem:
| Subsystem state | Endpoint configured? | Resolves to | |---|---|---| | Enabled in this process | n/a | nil — the subsystem's own Mount() installs its in-process Client | | Disabled | Yes (CLOUD_<NAME>_ZAP_ADDR) | ZAP RPC client targeting the endpoint | | Disabled | No | clients.Disabled<Name>() — fail-closed stub returning a clear error | | Payments / Vault | (always RPC per HIP-0106 solo-vault CDE) | ZAP RPC client at CLOUD_PAYMENTS_ZAP_ADDR / CLOUD_VAULT_ZAP_ADDR, or fail-closed disabled stub if no endpoint |
Subsystem code calls deps.<Name>.<Method>(...) without knowing the mode. The interface is the contract. Detection of the fail-closed stub at mount-time uses clients.IsDisabled(err).
The cloud/types leaf package holds the transport types AND the client interfaces so the in-process / RPC implementations can satisfy them without an import cycle through cloud. As subsystems ship their .zap schemas and zapc generate <schema>.zap --lang go --out ./zap/gen/ produces typed bindings, the placeholder types in cloud/types/ become aliases to the generated structs.
The unified binary is opinionated. Every subsystem uses the same Go substrate. One way to do everything. No parallel frameworks, no parallel ORMs, no parallel loggers, no parallel wire formats.
| Concern | Canonical | Notes | |---|---|---| | Web framework | hanzoai/zip | The ONE Go web framework. Built on Fiber v3 / fasthttp (implementation detail). Sinatra/Express-style primary API. No .Fast escape hatch — zip is fast. | | ORM | hanzoai/orm | Backends: SQLite (today), SQL/MySQL/MSSQL/Oracle via dbx (wiring pending), ZapDB via luxfi/database, CR-SQLite for client-side distributed (license-permitting). | | Logger | luxfi/log | NEVER uber-go/zap, NEVER log/slog, NEVER stdlib log. | | Wire protocol | ZAP | Every subsystem ships <svc>/schema/<name>.zap. zapc generates Go/TS/Py/Rust bindings. No .capnp files in Hanzo-authored source. | | Storage durability | hanzoai/replicate over hanzo/vfs | Per HIP-0107 (streaming replication over vfs). Covers SQLite WAL, ZapDB log, blockchain state, generic logs through one pipeline. | | Object store interface | hanzo/vfs | The ONLY object-store interface. All sink-side bytes route through vfs. | | Analytical store | hanzoai/datastore-go | Separate from orm. NOT an ORM backend — different workload class. |
Migration adapters (transitional only, not parallel ways to build new services): zip.AdaptNetHTTP(http.Handler), zip.AdaptNetHTTPFunc(http.HandlerFunc), zip.AdaptNetHTTPMiddleware(func(http.Handler) http.Handler), and app.Mount(prefix, http.Handler) as the path-prefixed mount form. chi.Router, gin.Engine, and beego.App.Handlers all satisfy http.Handler natively, so the same adapter covers every legacy framework — no per-framework wrapper needed. These exist so existing chi/beego/gin code can be wrapped into a zip mount without a same-day rewrite. New code must be written natively against zip. Adapters cost ~5% per-request perf versus native Fiber dispatch and that cost compounds at high RPS; replace adapted routes when feasible.
The framework itself is hanzoai/zip v0.1.0+ on feat/fiber-v3 (commit train rebuilds upstream zeekay/zip from scratch on Fiber v3; see hanzoai/zip PR #1). zip is the ONE web framework. chi / gin / beego / echo are MIGRATION-ONLY and never appear in new Hanzo Go code.
cloud is a single process exposing:
:8080) that fans out to subsystem handlers by URL prefix (/v1/iam/..., /v1/base/..., etc.)
:9653) for service-to-service authand inter-subsystem calls when split across binaries
:9090:8081 (gated by IAM admin role)Subsystem activation:
cloud \
--enable=iam,base,kms,commerce,gateway,ai,o11y \
--brand=osage \
--domain=osage.cloud \
--iam-issuer=https://iam.hanzo.id \
--kms-master-key-ref=kms://hanzo/cloud/osage/master \
--data-dir=/var/lib/cloud
Each subsystem flag enables that subsystem's HTTP routes, ZAP services, and background workers. Disabled subsystems contribute zero runtime cost beyond the compiled code in the binary.
Each Hanzo Go service exposes a single Mount(app *zip.App, deps cloud.Deps) error function (canonical signature; the old chi.Router-based shape is migrated through the zip.AdaptChi adapter). cloud's main.go is essentially:
import (
"github.com/hanzoai/zip"
"github.com/hanzoai/iam"
"github.com/hanzoai/base"
"github.com/hanzoai/kms"
"github.com/hanzoai/commerce"
"github.com/hanzoai/ai" // LLM control plane subsystem (was hanzoai/cloud pre-rename)
"github.com/hanzoai/gateway"
"github.com/hanzoai/o11y"
"github.com/hanzoai/vfs"
"github.com/hanzoai/mq"
"github.com/hanzoai/dns"
"github.com/hanzoai/amqp"
"github.com/hanzoai/mcp"
"github.com/hanzoai/cloud"
)
func main() {
cfg := cloud.LoadConfig()
deps := cloud.BuildDeps(cfg)
app := zip.New()
if cfg.Enabled("iam") { iam.Mount(app, deps) }
if cfg.Enabled("kms") { kms.Mount(app, deps) }
if cfg.Enabled("base") { base.Mount(app, deps) }
if cfg.Enabled("commerce") { commerce.Mount(app, deps) }
if cfg.Enabled("ai") { ai.Mount(app, deps) }
if cfg.Enabled("gateway") { gateway.Mount(app, deps) }
if cfg.Enabled("o11y") { o11y.Mount(app, deps) }
if cfg.Enabled("vfs") { vfs.Mount(app, deps) }
if cfg.Enabled("mq") { mq.Mount(app, deps) }
if cfg.Enabled("dns") { dns.Mount(app, deps) }
if cfg.Enabled("amqp") { amqp.Mount(app, deps) }
if cfg.Enabled("mcp") { mcp.Mount(app, deps) }
// ...
cloud.Serve(app, cfg) // HTTP + ZAP RPC + admin
}
This means every existing Go service must expose a Mount function in addition to its cmd/<service>/main.go. The main.go becomes a thin shim that calls Mount on its own; cloud's main calls the same Mount from its own binary. No business logic moves.
Realized refinement: the explicit
if cfg.Enabled(...)enumeration above shipped as the registry pattern —cloud.Register(name, order, mount)from each subsystem'sinit()plus a blank import incmd/cloud, withMountAll/ShutdownAlldriving ordered mount and reverse-ordered teardown. See "Realized state" above. TheMountcontract itself is unchanged.
Reference implementations already in tree (per AUDIT_2026_05_19.md): commerce, gateway, vfs — these export Mount(...) today. New subsystems join by mirroring their shape.
Every subsystem ships a public .zap schema describing its in-process and over-wire interface. Build pipeline:
<subsystem>/
schema/<name>.zap # public typed interface
zap/gen/ # zapc-generated bindings (Go for our use, others optional)
<name>.go # generated structs + interfaces
<name>_server.go # server-side handlers
<name>_client.go # typed client (in-process or RPC)
Makefile / build.sh # `zapc generate <schema>.zap --lang go --out ./zap/gen/`
zapc is the Rust-implemented Hanzo ZAP compiler at ~/work/hanzo/zap/zapc-rs/. Multi-language codegen: zapc generate file.zap --lang {go,ts,py,rust}.
When cloud runs subsystems in-process: each Mount(...) registers its *_server.go against the local ZAP dispatcher, and the typed client interfaces in deps resolve to direct Go function calls through the ZAP runtime (no marshalling, no network). When subsystems are split: same generated client makes ZAP RPC calls over the wire. Same business code in either mode.
Existing .zap schemas in production (real, not aspirational):
~/work/hanzo/ai/ai.zap (was ~/work/hanzo/cloud/cloud.zap pre-rename) — LLM control plane types (ChatMessage, ModelProvider, ChatCompletion)~/work/hanzo/commerce/api/billing/billing.zap — billing schemas~/work/hanzo/tasks/schema/tasks.zap — task scheduling~/work/hanzo/zap/rust/schema/zap.zap — self-describing schemaSubsystems with generated bindings under <svc>/zap/: python-sdk/proto/zap, kv/modules/zap, playground/internal/zap, hanzo.ai/components/zap, platform/pkg/zap, mcp/dist/zap, dev/hanzo-dev/zap, docdb/internal/zap, vector/src/zap. At least 10+ subsystems already on the pattern.
Policy: no .capnp files. No "Cap'n Proto" identifiers in Hanzo-authored source. ZAP is Hanzo-native and is the only thing the public surface knows about. (Brand-policy parity with @hanzo/gui / Zen MoDE — see CLAUDE.md.)
The cloud binary is fully stateless within the process. All state lives in the per-tenant SQLite + KMS + VFS layer per HIP-0302 / HIP-0107. Any replica can serve any request as long as it can open the requesting tenant's SQLite file from local storage (cached) or the encrypted S3 mirror (cold).
load balancer (k8s Service / DO LB / Cloudflare)
└── sticky / consistent-hash on X-Org-Id
↓
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ cloud replica│ │ cloud replica│ │ cloud replica│ ← N replicas, identical
│ ingress │ │ ingress │ │ ingress │ binary, no shared
│ gateway │ │ gateway │ │ gateway │ in-process state
│ iam, kms, │ │ iam, kms, │ │ iam, kms, │
│ base, │ │ base, │ │ base, │
│ commerce, │ │ commerce, │ │ commerce, │
│ ai, vfs, │ │ ai, vfs, │ │ ai, vfs, │
│ o11y, mcp, │ │ o11y, mcp, │ │ o11y, mcp, │
│ amqp, mq, │ │ amqp, mq, │ │ amqp, mq, │
│ dns, authz │ │ dns, authz │ │ dns, authz │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└─────────────────┼─────────────────┘
↓
┌──────────────────────────────────────┐
│ per-tenant SQLite + age-encrypted │
│ S3 mirror (HIP-0302, HIP-0107). │
│ KMS master key + per-org HKDF DEK. │
│ VFS = canonical object-store iface. │
└──────────────────────────────────────┘
The load balancer routes external requests to any replica. Consistent-hash on X-Org-Id (after gateway-mint) keeps each tenant warm on one replica when possible — this is a perf optimization, not a correctness requirement. Cold tenants get served by whichever replica has capacity; per-tenant SQLite cold-open is millisecond-scale.
Within each replica, the gateway subsystem owns the edge concerns:
iam JWKSX-Org-Id per iam.tenantClaimAfter gateway, calls between subsystems use ZAP-typed Go method calls (see "Subsystem boundaries"); no per-hop JSON marshalling. JSON marshal runs at most once per request, at the subsystem handler boundary, returning to the gateway response path.
| Workload | RAM per replica | Throughput per replica | |---|---:|---:| | Idle | ~150 MB | n/a | | 100 active tenants, mixed | ~500 MB – 1 GB | ~10K req/s | | 1000 active tenants, heavy | ~7 – 16 GB | ~50K req/s |
Horizontal scaling knob: number of replicas behind the load balancer. Kubernetes HPA on CPU + per-replica X-Org-Id-affinity hashing scales linearly until the per-tenant SQLite + replicate fan-out hits its own limits, which sit well above this binary's per-replica throughput.
Cold-start budget: ~150 ms full boot with all 13 subsystems mounted; the lazy-mount pattern from HIP-0108 brings this down to ~10 ms by deferring subsystem Mount() until first request.
The verified, measured budget for a Hanzo zip-on-fasthttp replica (Apple M1 Max / Go 1.26.3 / Fiber v3 v3.2.0, gateway hot-path):
| Conns | Heap delta | Per-conn heap | Goroutines | |---|---:|---:|---:| | 1,000 | 7.84 MiB | 8.02 KiB | 1.00 / conn | | 8,454 | 67 MiB | 8.02 KiB | 1.00 / conn |
| Concurrent conns | Heap | Pod RAM (with headroom) | |---:|---:|---:| | 10,000 | 80 MiB | 256 MiB | | 100,000 | 800 MiB | 1 GiB | | 1,000,000 | 8 GiB | 10 replicas × 1 GiB |
The operational target is 100k concurrent client connections per replica at a 1 GiB pod ceiling. The cliff above 100k/replica is OS-side (fasthttp listener queue, ulimit, kernel ephemeral port range), not Go-side. Cap zip.Config.Concurrency: 100_000 to stay inside the budget; the kernel ulimit must be configured accordingly via the pod's securityContext.
JSON impl at the edge: every production cloud Dockerfile compiles with GOEXPERIMENT=jsonv2. Verified edge-path wins on Apple M1 Max / Go 1.26 / zip on Fiber v3 v3.2.0:
| Bench | json/v1 | json/v2 | Δ | |---|---|---|---| | Edge POST roundtrip | 19,782 ns / 73 allocs | 17,406 ns / 56 allocs | −12% time, −23% allocs | | Marshal-only | 12,860 ns / 34 allocs | 10,092 ns / 34 allocs | −22% time | | Unmarshal-only | 17,122 ns / 67 allocs | 13,785 ns / 50 allocs | −19% time, −25% allocs |
JSON marshal runs at most once per request (at the gateway edge); inter-subsystem calls use ZAP. The −22% marshal-only win lands once, on the response path to the client.
Reproduce:
cd ~/work/hanzo/zip
go test -bench=BenchmarkJSON -benchmem -run=^$ .
GOEXPERIMENT=jsonv2 go test -bench=BenchmarkJSON -benchmem -run=^$ .
cd ~/work/hanzo/gateway
go test -mod=mod -run=TestConnMemory -v -conn-count=10000
Canonical scale doc: ~/work/hanzo/hips/docs/SCALE_STANDARD.md.
By design, the cloud binary holds no per-tenant state in process memory beyond:
pragma cache_size-bounded)Replica restart is safe at any time. Active requests reconnect via the load balancer. No graceful-drain coordination required across replicas beyond standard k8s pod lifecycle.
→ causes memory leak under tenant churn
topics) → use durable queue subsystem (mq, amqp) instead
load balancer config; breaks the "any replica serves any tenant" invariant. The X-Org-Id consistent hash is an optimization, not a contract.
| Layer | Today | Under HIP-106 | |---|---|---| | Auth | Per-domain IAM (multi-tenant) | Unchanged. IAM resolves (app, org) from origin; subsystems see JWT claims via X-Org-Id. | | Storage | Per-service postgres or per-tenant SQLite (varies) | Always per-tenant SQLite ({data-dir}/orgs/{org}/{service}.db) with per-org DEK per HIP-0302. No postgres in the unified path. | | KMS | Per-org master keys today | Unchanged. KMS in-process serves kms://<deployment>/<org>/<purpose> references. | | Cache | Per-service KV (varies) | Process-local LRU or per-org sub-buckets in shared in-memory cache. No KV. | | Brand | Per-domain detection in app code | cfg.Brand at startup; subsystems read brand from deps; UI templates resolved from embedded assets. |
In the unified binary, IAM, KMS, Base, Commerce, Cloud, etc. live in the same process address space. Inter-subsystem calls are ZAP-typed Go function calls via the deps:
type Deps struct {
IAM iamzap.Client // generated from iam/schema/iam.zap
KMS kmszap.Client // generated from kms/schema/kms.zap
Base basezap.Client // generated from base/schema/base.zap
Commerce commercezap.Client // generated from commerce/schema/commerce.zap
Cloud cloudzap.Client // generated from cloud/schema/cloud.zap
O11y o11yzap.Client // generated from o11y/schema/o11y.zap
BrandCfg brand.Config
}
Each generated Client has two implementations:
cloud wires in-process. Legacy split deploys wire RPC. Same business code in either mode. Every subsystem boundary is typed end to end via the .zap schema.
User-supplied code inside the unified binary uses the in-process extension runtime from HIP-0105:
goja wins multi-tenant SaaS at scale)
native (Hanzo-authored)
The extension runtime IS the user-code boundary inside the unified process. Don't shell out, don't spawn sidecars, don't run Knative pods for per-request hooks. HIP-0060 (Functions) handles the workloads where those overheads are justified.
HIP-0105 defines two mount points (in-process Base hooks AND web routes via hanzoai/zip). The unified binary uses mount point #2 to allow service routes themselves to be authored in any HIP-0105-supported language:
| Language | Runtime name | Mount path | |---|---|---| | Go (Hanzo-authored) | native | direct in-process — default for all service code | | Rust / C++ / Zig / AssemblyScript | wasm (wazero) | compiled to *.wasm, loaded under hz_routes/<name>/ | | JavaScript / TypeScript | goja (production) or v8go (experimental) | source files loaded directly | | Python | pyvm (single-tenant only, -tags pyvm) | CPython 3.13 sub-interpreter pool | | DSL / policy | starlark | sandboxed, deterministic, no I/O by default |
Every HIP-0105 runtime is mountable as a route via the SAME entry point: app.Module(method+path, runtimeName, modulePath). One method on the zip.App; one canonical JSON envelope shape ({method, path, params, query, headers, body, org, user, userEmail}) every guest sees; one response envelope ({status, headers, body}) every guest returns. There is no per-runtime mount API — app.ModuleWasm / app.ModuleGoja / app.ModulePython do not exist on purpose. One way.
app.Module("POST /v1/policy/eval", "wasm", "./ext/policy") // Rust/AS/Zig/C → wasm via wazero
app.Module("POST /v1/transform", "pyvm", "./ext/transform") // single-tenant CPython
app.Module("POST /v1/webhook", "goja", "./ext/webhook") // recommended multi-tenant JS
app.Module("POST /v1/route", "starlark", "./ext/route") // config DSL
The route author writes their handler in their preferred language; the same .zap schemas generate the same I/O types across all of them. Crash isolation, cancellation semantics, and scale characteristics are exactly as described in HIP-0105. The loader is duck-typed via zip/runtime.Loader so zip itself stays decoupled from hanzoai/base/plugins/extruntime (the canonical loader implementation); the unified binary constructs *extruntime.Loader once with whichever runtimes it cares about and threads it into zip.Config.Loader.
Multi-tenant deployments MUST gate AllowedRuntimes on zip.Config to exclude runtimes lacking hard sandbox (pyvm, v8go). AllowedRuntimes: nil accepts whatever the Loader has registered.
The credential for programmatic access is an API key. IAM mints and verifies it, gateway resolves it to a principal at the edge, and no subsystem downstream reads it again. There are two types, and the prefix is what every consumer switches on:
| Type | Prefix | Where it belongs | |---|---|---| | publishable | pk- | browser and client code; identifies an org, authorizes nothing that spends | | secret | sk- | server side only; carries the caller's full scope |
hk- is the older name for a secret key. Verification still accepts one; nothing has minted one since iam v1.33.9.
GET /v1/keys # the caller's keys: { type, prefix, createdAt }
POST /v1/keys # mint or rotate the key of { type }
DELETE /v1/keys # revoke the key of that type
The secret is returned exactly once, at mint. Every later read returns type, prefix and creation time. What is stored is a hash of the key, never the key — a plaintext credential at rest is a defect, not a configuration choice. The subject a key is minted for is derived from the validated identity headers and never from the request body, so a caller can mint and revoke only their own.
A key carries a scope, and the scope is enforced at the gateway edge before any subsystem sees the request:
| Field | Meaning | |---|---| | models | which models the key may address; * for all | | services | which subsystems it may invoke — llm, agents, mcp, embeddings, images, audio | | rateLimit | requests per minute; default 60 | | spendLimit | ceiling in USD cents per billing period; unset means the org balance is the only ceiling | | allowedIPs | CIDR allowlist; unset means any address | | expiresAt | expiry; unset means the key lives until revoked |
Spend is prepaid. An org buys credits and every priced request draws the balance down, so spend cannot exceed what was bought and there is no surprise bill. One credit is one US cent — the arithmetic and the display are the same unit. One balance covers every subsystem: inference, agent runs, MCP tool calls, storage.
commerce owns the balance. The metering client is the one place this binary asks about money, and it wraps every priced request in two calls:
Insufficient balance is 402 insufficient_balance and the handler never runs. An unreachable commerce is 503 balance_unavailable.
actually consumed.
Fail-closed is the default: when commerce cannot be reached the request is denied rather than served unpriced. Fail-open is a deliberate per-deployment choice, never the consequence of an outage. Free routes declare a price of zero and skip both calls.
A usage event says who spent, what served it, what it consumed and how it went. It carries no prompt content and no PII.
type UsageEvent struct {
ID string
Timestamp time.Time
// Who spent
OrgID string
ProjectID string
UserID string
KeyID string
// What served it
Service ServiceScope
Model string
Provider string // the upstream that actually answered
// What it consumed
PromptTokens int
CompletionTokens int
TotalTokens int
Cost int64 // USD cents
// How it went
LatencyMs int64
TTFTMs int64 // time to first token, streaming
Status UsageStatus
// Agent runs attribute each tool call separately
AgentID string
AgentRunID string
ToolCalls []ToolCall
Metadata map[string]string
}
type ToolCall struct {
Name string // tool
Provider string // MCP server that served it
DurationMs int64
Status UsageStatus
}
Events are written to the calling tenant's own store, so a usage read is org-scoped by construction rather than by a WHERE clause, and commerce aggregates them for invoicing and reseller revenue share.
GET /v1/usage/summary # current billing period
GET /v1/usage/timeseries # hourly, daily or monthly buckets
GET /v1/usage/by-model
GET /v1/usage/by-user
GET /v1/usage/by-key
GET /v1/usage/events # paginated raw events
All six accept start, end, granularity and filter. An alert is the same data read against a threshold — spend, request count or error count, over a daily, weekly or monthly period — delivered to a webhook, an email address or a Slack hook when it is crossed.
The inference surface is OpenAI's, so an existing application moves by changing two strings:
from openai import OpenAI
client = OpenAI(
api_key="sk-...",
base_url="https://api.hanzo.ai/v1",
)
response = client.chat.completions.create(
model="zen-8b-instruct",
messages=[{"role": "user", "content": "Hello"}],
)
POST /v1/chat/completions # streaming and non-streaming
POST /v1/completions # legacy
POST /v1/embeddings
POST /v1/images/generations
POST /v1/audio/transcriptions
POST /v1/audio/translations
GET /v1/models
The ai subsystem holds the model catalog and the routing policy; gateway speaks the OpenAI shape at the edge and absorbs provider-specific differences. Both are subsystems of this binary, so a request arriving at /v1/chat/completions is authenticated, authorized against the balance, routed, served and recorded without leaving the process.
| Deployment | Brand | Enabled subsystems | Domain | |---|---|---|---| | Hanzo flagship | hanzo | all | api.hanzo.ai | | Osage Cloud | osage | iam, base, kms, commerce, ai, gateway, o11y, vfs | api.osage.cloud | | Lux Cloud | lux | iam, base, kms, gateway, chain | api.lux.cloud | | Zoo Cloud | zoo | iam, base, kms, ai, gateway, vfs | api.zoo.cloud | | Customer X (reseller) | custom | iam, base, kms, commerce, gateway | api.x.com |
Same image. Different startup configuration. The osage.cloud marketing site that shipped 2026-05-19 gets a real backend when this HIP lands.
Multi-week real engineering. Order revised from AUDIT_2026_05_19.md findings:
Per AUDIT_2026_05_19.md, three subsystems already export Mount(...): commerce, gateway, vfs. These are the reference impls — Phase 1 first reshapes their signatures to the canonical (*zip.App, Deps) error form, then extends the same shape to the smallest remaining services:
Reference impls (already Mount-shaped):
IAM-integrated, Mount exists. Reshape Deps to the canonical interface and use it as the live blueprint.
Mount exists. Validate that the subsystem can beboth standalone and embedded.
Mount exists. Smallest of the three.Smallest remaining services to bring to the Mount contract:
Mount; clean deps.deps.IAM.Explicitly excluded from Phase 1 (and from the unified binary entirely):
vault — PCI-CDE. Folding it into a multi-tenant processexpands PCI scope to every other tenant. Stays its own deployment. See Non-goals.
cloud (1 week)~/work/hanzo/cloud/cmd/cloud/main.go — new repo github.com/hanzoai/cloud. Imports the seven subsystems from Phase 1, wires Deps, dispatches by subsystem flag. Ship as ghcr.io/hanzoai/cloud:v0.1.0. Single-tenant smoke deployment at a dev domain first.
plugins/extruntime, plugins/jsvm, plugins/wasmvmmust remain isolated per-org (per HIP-0105)
per audit). Get this right and the per-tenant routing pattern is proven for everything else.
Universal X-Org-Id gating at the unified mux entry. Per-tenant data isolation enforced by Base. KMS reference resolution scoped to the calling org. Telemetry tagged with org.
Operator CRD ResellerCloud{name, parentOrg, brand, enabledSubsystems} that creates the IAM app + KMS namespace + Base storage allocation + Gateway routes atomically. Documented at ~/work/hanzo/cloud/docs/RESELLER.md. Self-serve resell becomes possible.
Operator convergence (BLOCKER for Phase 5). The audit identified three parallel operator lineages:
~/work/hanzo/operator (Go)~/work/hanzo/hanzo-operator (Go, production)~/work/hanzo/operator-core (Rust)Phase 5 cannot ship until one is picked and the other two are either folded in or archived. Recommended path: keep hanzo-operator (the production lineage with the existing ClusterRole), fold the ResellerCloud CRD into it, archive operator/, and demote operator-core to a Rust library (library-no-action per audit) for shared CRD types. Decision required before Phase 5 starts.
Commerce subsystem reads per-tenant usage from base + o11y/Datastore and applies the reseller revenue-share contract. commerce is the canonical billing/pricing/subscription home — the TS billing and pricing packages get absorbed into commerce via Go rewrites.
Note on auto: the earlier audit grouped auto with billing/pricing for commerce absorption. This was wrong. ~/work/hanzo/auto is a plug-n-play IFTT-style trigger framework that sits on top of ~/work/hanzo/tasks (durable task queue). It is NOT under commerce. auto is its own subsystem and mounts independently. tasks is its own subsystem — the durable-queue primitive used by auto, by commerce recurring billing, by o11y retention jobs, and by anything else that needs durable scheduling.
Commerce is a thin orchestrator. It owns the customer-facing checkout flow, tenant config, billing logic, pricing logic, invoicing, and webhook intake. It explicitly does NOT:
Adyen SDK)
Commerce only handles tokens (vault tokens) and intents (payments-orchestrator references). When commerce needs to charge, it calls payments (Rust) via ZAP RPC with a token + amount + processor hint. Payments calls vault (Go) via ZAP RPC with a "Charge this token" request; vault pulls the PAN from its encrypted store, makes the outbound HTTPS to the processor, and returns the response. PAN never leaves vault.
This makes commerce CDE-connected, not CDE. Lighter controls apply (network segmentation, access control, change management) but commerce is NOT subject to PCI-DSS L1 audit.
Vault is the only system in PCI-CDE. Per the corrected scope:
| System | PCI scope | |---|---| | vault | CDE — the only system that touches PAN. Full L1 audit. Quarterly ASV. HSM-backed key store. Own deployment, own k8s namespace, own NetworkPolicy boundary. | | payments | CDE-connected (NOT CDE). Sees only tokens. Payments service operated in tokens-only mode. Calls vault.Charge(token, processor, amount) for the actual processor call. | | commerce | CDE-connected (NOT CDE). Light router. Only ever handles tokens + intent IDs. Mounts inside cloud like any other subsystem. | | Everything else in cloud | Not CDE-connected. Standard SOC2-grade controls. |
For this architecture to be sound, two requirements must hold:
ships a vault-collect.js iframe; PAN posts directly to vault from the browser, never via commerce or any Hanzo app server).
audit of payments data flow that no code path exposes raw PAN to the surrounding Go process.
Both are tracked under the implementation TODO list at ~/work/hanzo/vault/docs/ (to be created).
The same architecture supports four deployment modes:
commerce. Hanzo bears the PCI-DSS L1 audit. PCI scope = vault only.
(lux.cloud, zoo.cloud, osage.cloud) runs commerce inside their cloud deployment; commerce's payments_client and vault_client ZAP endpoints point at Hanzo's payments + vault. Customer carries no PCI obligation. Hanzo's vault has multi-tenant token namespacing per org.
vault + payments. Their cloud's commerce subsystem points its ZAP-RPC endpoints at THEIR vault + payments deployment. Hanzo carries no PCI obligation for that customer's flows. The customer holds their own PCI scope. Commerce is a swappable thin router.
commercial unit is payment-processing. Deploy payments + vault + a trimmed cloud as a unit. Commerce still operates as light router — no design change, only deployment shape.
Modes 1-3 share the same binary. Configuration determines which endpoints commerce talks to. The "swappable thin router" property is load-bearing: commerce never grows code that depends on a specific vault or payments operator.
Dokploy fork), brain (where still TS), bot, billing, pricing (separate Go rewrites slated under commerce subsumption). They can be ported to Go later if performance or operations require it; this HIP does not block on them.
vault and payments are explicitly NOT folded. Both stay astheir own deployments with their own PCI scope boundaries per the "Solo-vault CDE" section above. Vault is CDE. Payments is CDE-connected. Commerce talks to both via ZAP RPC. Hard rule, no exceptions — even single-tenant deployments use the three-process architecture.
~/work/hanzo/flow (Hanzo Flow — visual ML pipeline / agent-building) is NOT folded. Visual MLpipeline / agent-building tool. Heavy native deps (torch, faiss, sentence-transformers). Runs as a separate process behind the gateway subsystem. Per the FT audit (2026-05-19), classified RED — defer to GIL-Python until torch ships cp313t (>=2.6).
~/work/hanzo/datastore (Datastore fork) is NOT folded. OLAP column store. Uses Datastore-native ReplicatedMergeTree + S3 disk. Out of scope. Shares S3 bucket with HIP-0107 streaming via vfs prefix (s3://bucket/datastore/... vs s3://bucket/replicate/...).
~/work/hanzo/insights (Hanzo Insights — AI observability + eval + prompt management) is NOT folded.** Runs as a separate process — the canonical AI console for cloud-hosted LLM operations. Integrates with cloud via HTTP + (forthcoming) ZAP-typed endpoints; consumed by the cloud subsystem (LLM control plane) and surfaced to operators as part of the AI console.
audit** (cli, erp, insights, sentry, studio) — all stay separate processes until their upstream FT-blockers clear (xmlsec, confluent-kafka, chdb, single-threaded Django/Celery assumptions, torch <2.6). Run under regular python3.13 (GIL-enabled) until then.
in-deployment service calls; cross-deployment (Hanzo ↔ Lux ↔ Zoo) stays on the existing service-discovery + bridge layer.
together. If a subsystem becomes a hot bottleneck, fall back to running it as its own binary alongside the unified one — the Mount contract supports both.
slower than a focused service. Acceptable for long-lived deployments; needs measurement for fast-restart scenarios. Suggest benchmarking on Phase 2.
Acceptable trade-off for resell where blast radius is per-customer anyway, but Hanzo flagship may want to keep some subsystems split.
/v1/iam/..., /v1/base/..., /v1/commerce/..., etc. Already mostly per-subsystem-prefixed. The unified mux compiles a route table at startup and refuses to start on collision.
every subsystem compiled in. For developer builds we might want to disable some at build time to shrink the binary. ANSWERED by HIP-0116: neither build tags nor bigger binaries — services build standalone AND as ZAP-native plugin VMs (luxfi/lpm-installed, dispatched over ZAP per HIP-0114). Build tags are explicitly rejected there; runtime enablement stays the CLI/env flags specified here.
.zap schema, how do we manage cross-version compat? Suggest semver on the .zap file itself (@version 1.2.0 directive) and zapc refusing breaking changes without an explicit major bump. Tracked for follow-up.
operator/, hanzo-operator/, operator-core/ (Rust). Audit flagged for convergence. Phase 5 (reseller provisioning) is blocked on picking one. Suggest dedicated session for the operator decision before Phase 5 starts.
Tailscale (one tailscaled), GitLab (omnibus)
~/work/hanzo/AUDIT_2026_05_19.md — ecosystem audit driving themigration order
today)
inside the unified binary)
the binary)
subsystem must satisfy)
Base Services (the per-tenant data isolation model — covers both Base's SQLite backend AND ZapDB-backed deployments)
pipeline; supersedes the per-backend sink code in HIP-0302's reference impl — substrate stays, transport unifies)
storage — the realized default)
balance-floor semantics gated at this binary's edge)
authentication for the ZAP contract this HIP mandates)
standalone / embedded / plugin VM; answers Open Question 4)
topologies of this binary)