HIP-120: ZAP-Native Transport & gRPC Elimination. Status Draft. Hanzo's own standard — read this before implementing against it.
Hanzo has exactly one inter-service transport: ZAP, the Hanzo-native binary wire (github.com/zap-proto/*). This HIP makes the corollary a platform-wide standard:
A Hanzo service speaks ZAP, HTTP, or WS. It never speaks gRPC.
The three permitted protocols are complete for every wire Hanzo needs — ZAP for inter-service and inter-VM traffic (HIP-0106, HIP-0114), HTTP for the external request/response edge and HTTP-semantics-over-ZAP internally, and WS for long-lived bidirectional client streams. gRPC is a fourth transport with its own HTTP/2 framing, its own code-generated stub layer, and its own mutable-message data model; admitting it would violate "one and only one way to do everything" and drag google.golang.org/grpc — a large dependency and attack surface — into the graph.
This HIP specifies: the rule and its RFC 2119 force; the three permitted protocols; OTLP-over-ZAP (the shipped exemplar — OpenTelemetry spans ride the ZAP wire to the o11y collector, standard OTLP protobuf payload, ZAP transport, never OTLP-HTTP :4318 or OTLP-gRPC :4317); the pb2zap source-migration path; the design decision that there is no transparent runtime gRPC-over-ZAP shim; and the canonical conformance gate — zero first-party google.golang.org/grpc imports, enforced by grep over .go source, not by go.mod.
The standard is already shipped across four repositories — cloud, gateway, visor, ai — in four commits (see Reference Implementation). First-party gRPC imports across all four are zero. What remains is never-dialed transitive library code, honestly accounted for in Residual & Conformance.
HIP-0106 fixed ZAP as the inter-subsystem contract of the unified cloud binary: every subsystem ships a .zap schema, zapc generates the bindings, and calls resolve to direct Go method dispatch when co-resident or ZAP RPC when split. HIP-0114 fixed ZAP as the inter-VM cognitive transport for Thinking Chains. Both rest on the same premise: ZAP is the one Hanzo wire. A second RPC transport — gRPC — is not a convenience, it is a fork in the road, and "if there are two ways to do something, one is wrong." gRPC is the wrong one: Hanzo already has a native, versioned, schema-driven transport that spans in-process, node-local, and cross-node hops with one set of generated types.
ZAP splits a message into three values with distinct lifetimes: an Input (built once, write-only), a []byte (the transport frame), and a View (read many times, zero-copy over the frame's backing bytes). Protobuf/gRPC collapses these into a single mutable message struct that is built, read, and mutated in place. The mutable model forces allocation and copy at every boundary and is precisely the model ZAP rejects. Bolting gRPC onto a ZAP stack re-imports the cost ZAP was built to remove.
Per HIP-0112, external traffic terminates at ingress → gateway: TLS ends at ingress, the gateway validates the IAM JWT, strips client-supplied identity headers, and mints trusted ones. Every hop after the gateway rides ZAP with the minted identity already attached. A gRPC service would need its own HTTP/2 metadata-based auth plumbing and its own interceptor chain kept in lockstep with the gateway's header contract — a second identity path to drift out of sync. One transport means one identity path.
google.golang.org/grpc pulls an HTTP/2 framing stack, connection/balancer machinery, and the protoc-gen-go-grpc stub layer; the OpenTelemetry gRPC exporters (otlptracegrpc, otlpmetricgrpc) and otelgrpc pull it further into telemetry. Each import is transitive CVE exposure and dependency weight for a wire Hanzo does not use. Eliminating first-party gRPC shrinks the graph to what Hanzo actually dials.
The keywords MUST, MUST NOT, SHALL, SHOULD, MAY are used per RFC 2119.
google.golang.org/grpc (or a generated *_grpc.pb.go stub) in first-party source.
traffic between Hanzo services.
and MAY additionally terminate HTTP and WS at the external edge (§2).
MUST prefer the provider's REST/HTTP surface and hand-roll a net/http client when a maintained REST client is unavailable (§Rationale; the visor/ai GCP compute adapters are the reference). This keeps the gRPC transport out of Hanzo's first-party graph.
| Protocol | Where it applies | Canonical implementation | |----------|------------------|--------------------------| | ZAP | The one internal wire: inter-subsystem (HIP-0106), inter-VM (HIP-0114), service-to-service RPC. Native binary frames with an X-Wing PQ-KEM handshake; carries build-once Input → []byte → read-many zero-copy View. | github.com/zap-proto/go (codec/runtime, v1.3.0), github.com/zap-proto/zip (web framework, v1.2.1), github.com/zap-proto/http (HTTP semantics over the ZAP wire, v0.2.0) | | HTTP | The external request/response edge (JSON in/out, terminated by ingress → gateway per HIP-0112). Internally, HTTP semantics ride the ZAP wire via zap-proto/http — the same request/response shape, ZAP frames underneath. | zap-proto/http, hanzoai/zip handlers; stdlib encoding/json(/v2) at the edge | | WS | Long-lived bidirectional client streams at the external edge; the ZAP-carried variant for internal streaming. | zap-proto/ws, zap-proto/web |
The rest of the ZAP family (zap-proto/zapd router, zap-proto/zap-spec) are part of the same ecosystem. Note the namespace: the ZAP transport and framework live under github.com/zap-proto/*; this is distinct from hanzoai/zap, the Hanzo SQL/KV/Datastore sidecar, which keeps the hanzoai namespace and is not a transport.
gRPC appears in none of these rows. There is no fourth row.
Telemetry is the canonical demonstration that a protocol conventionally bound to gRPC can ride ZAP with no semantic change. OpenTelemetry ships an otlptrace.Client interface whose default wires are OTLP-over-gRPC (:4317) and OTLP-over-HTTP (:4318). Hanzo implements a third client whose wire is ZAP.
cloud/zaptrace/zaptrace.go is an otlptrace.Client (compile-time asserted: var _ otlptrace.Client = (*Client)(nil)) whose transport is github.com/zap-proto/http:
the transport differs — the collector receives a byte-identical ExportTraceServiceRequest.
UploadTraces builds a fasthttp POST http://zap/v1/traces with Content-Type: application/x-protobuf and sends it through a pooled zaphttp.Transport. The collector's ZAP receiver (zapreceiver) terminates the frame at :4319 and writes to hanzoai/datastore. Spans travel OTLP-over-ZAP — never OTLP-HTTP :4318, never OTLP-gRPC :4317.
repeated ResourceSpans resource_spans = 1, so UploadTraces appends each ResourceSpans under field 1 with protowire directly from the grpc-free go.opentelemetry.io/proto/otlp/trace/v1 messages. This deliberately avoids the generated collector/trace/v1.ExportTraceServiceRequest type, whose sibling trace_service_grpc.pb.go (shipped with no build tag) would drag google.golang.org/grpc back into the module graph. The hand-encoding is byte-identical to the generated marshaler — proven by zaptrace_test.go, which stands up a real zaphttp.Server and decodes the received body with the canonical collector/trace/v1 type.
cloud/cmd/cloud/telemetry.go installs exactly one tracer provider:
OTEL_EXPORTER_ZAP_ENDPOINT set (or unset with a ZAP default of localhost:4319) → spans ride ZAP.
OTEL_EXPORTER_OTLP_* endpoint set → spans ride OTLP-HTTP(the interop/loopback fallback used while the standalone collector is folded in). This fallback is HTTP, never gRPC.
cloud unsets OTEL_EXPORTER_OTLP_TRACES_ENDPOINT / OTEL_EXPORTER_OTLP_ENDPOINT so an embedded subsystem (notably hanzoai/ai's object.InitTelemetry) cannot install a second, competing OTLP provider that would strand later spans on :4318. Exactly one provider, one wire, deterministically.
Scope note (honest): traces ride ZAP today. A ZAP-native metrics/logs exporter is roadmap (§Residual & Conformance); the OTLP metrics/logs exporters are the current transitive-grpc source, never a dialed gRPC channel.
Porting a protobuf/gRPC call site to ZAP is a source-level transformation, accelerated by github.com/zap-proto/pb2zap — a syntactic go/ast codemod (a one-time migration tool, not a runtime bridge). For each construction site it:
&x_pb.Req{F: v} into the ZAP build-once form xwire.NewReq(xwire.ReqInput{F: v}).
xwire import and drops the now-unused x_pb import. mechanically transform (a field read becomes a View accessor; an in-place mutate has no ZAP equivalent and must be restructured by hand).
pb2zap moves the mechanical 80%; the reported read/mutate sites are finished by hand against the ZAP View/Input types. There is no automatic path for a mutate-in-place site because ZAP has no mutable message (§Rationale).
The canonical, CI-enforceable gate is: zero first-party google.golang.org/grpc imports.
# MUST return no rows for a conforming repo:
grep -rl '"google.golang.org/grpc' --include='*.go' . | grep -v /vendor/
Verified empty for cloud, gateway, visor, and ai.
The gate is deliberately not grep google.golang.org/grpc go.mod. That command is non-empty and cannot be driven to zero while Hanzo depends on the upstream OpenTelemetry OTLP-HTTP exporter and the Gemini SDK, both of which list gRPC as an indirect requirement they never dial (§Residual & Conformance). A go.mod gate would be permanently red for reasons unrelated to whether any Hanzo code speaks gRPC. The import gate measures the real invariant: does first-party Hanzo code dial or serve gRPC? No.
The conformance gate is a grep over source, suitable for CI. A regression — a new dependency that imports google.golang.org/grpc first-party, or a "convenient" gRPC client added to a service — fails the gate deterministically, where a human reviewer might miss it in a large diff.
Enforceable gate: PASS. First-party google.golang.org/grpc imports across cloud, gateway, visor, ai = zero (verified by the §5 grep). No Hanzo first-party code serves or dials gRPC.
Residual go.mod grpc is never-dialed transitive/indirect, and is not a conformance signal. Per repo:
| Repo | grpc in go.mod | Source (never dialed by first-party code) | |------|------------------|-------------------------------------------| | cloud | v1.81.1 // indirect (+ otelgrpc v0.67.0 // indirect) | Embedded siblings: hanzoai/ai, hanzoai/o11y (intrinsically an OTLP/gRPC collector, so its gRPC is definitional to the receiver, not a Hanzo client dial), hanzoai/base (GCS gRPC transport). zaptrace itself is grpc-free. | | gateway | v1.80.0 // indirect (+ otelgrpc v0.67.0 // indirect) | krakend-pubsub's hardcoded gocloud.dev/pubsub/gcppubsub blank import (a GCP Pub/Sub driver Hanzo does not use), legacy build only. | | visor | v1.80.0 // indirect | OTLP-HTTP telemetry exporter: telemetry → otlpmetrichttp → internal/oconf → grpc. HTTP transport; no channel dialed. | | ai | v1.81.1 // indirect | OTLP-HTTP exporter (object → otlptracehttp → internal/otlpconfig → grpc) and the Gemini SDK google.golang.org/genai (a direct dep at v1.10.0 whose HTTP backend transitively requires grpc). Neither dials a gRPC channel. |
Gate blind spot, stated honestly. The import grep catches direct first-party gRPC use; it does not catch a first-party import of a third-party SDK that internally dials gRPC to an external provider. The mandate governs Hanzo's own inter-service wire — a third-party cloud provider's control-plane SDK is interop, not a Hanzo transport. The governing rule: where a REST/HTTP surface exists, the gRPC SDK MUST be replaced (done for GCP compute in visor/ai); where none exists, the dependency is a documented exception that never targets a Hanzo service. No such exception exists in-tree today (verified: visor carries no external gRPC-dialing SDK).
Path to literal-zero go.mod (future work). Reaching an empty grep grpc go.mod requires eliminating the upstream transitive pulls, none of which is a Hanzo gRPC conversation:
zaptrace pattern (protowire hand-encode over zap-proto/http) to a metrics and logs exporter, retiring otlpmetrichttp/otlptracehttp — the OTLP-HTTP internal config that carries the indirect grpc in visor and ai.
ai). Replace google.golang.org/genai with a net/http client against the generativelanguage REST surface, removing the last direct dependency that transitively requires grpc.
cloud). The o11y collector is an OTLP/gRPC receiver by definition; its grpc is not a client dial and is out of scope for the client-side mandate. hanzoai/base GCS transport is a separate GCS- over-REST question tracked with the compute work.
Until then, the standard's conformance is measured where it is real and enforceable: zero first-party gRPC imports.
.capnp in Hanzo-authored source" policy.ingress → gateway → services; the external edge where HTTP/WS terminate and identity headers are minted.github.com/zap-proto/{go,zip,http,ws,web,zapd,zap-spec} — the ZAP transport and framework ecosystem.github.com/zap-proto/pb2zap — the syntactic go/ast protobuf→ZAP migration codemod.Cloud is one binary running plugins. When two of them sit on the same machine — which is the normal case, not the exception — a TCP hop to localhost is pure overhead: a syscall path, a checksum, a port, and a loopback round trip to reach a process sharing the same kernel.
ZAP over a unix socket is the PREFERRED transport for any plugin-to-plugin call inside cloud. Network ZAP remains correct where the peer is genuinely on another machine.
The rule, in order:
A socket path is also an authorization boundary: filesystem permissions gate who may connect, with no port to scan and no interface to accidentally bind. A service reachable only through a socket in its own namespace cannot be reached from another pod at all.
Shipped: cloud/rpc.go — the internal plane over net.Listen("unix", path) with a 200ms dial probe to detect a live peer before binding, and dial.go/serve.go moving callers onto it (branch feat/zap-uds-internal-plane).
What UDS cannot do, so it is not claimed: a per-node DaemonSet reading /var/log/pods does not share a filesystem namespace with a central service. That path stays network ZAP unless the reader becomes co-located with its consumer. UDS is chosen because processes are co-located; it never makes them so.
Copyright and related rights waived via CC0.