ZAP Protocol
SDKs

Go

ZAP Go SDK — the canonical zero-copy wire runtime, capability layer, and zapgen RPC codegen.

Go Binding

github.com/zap-proto/go is the canonical Go runtime for the ZAP wire format. It is pure standard library — zero external dependencies — and is the reference every other language runtime is checked against: a buffer built here parses unchanged through the TypeScript and Python runtimes, and vice versa (pinned by a shared golden vector).

The module is the read side (Parse / Message / Object / List), the write side (Builder / ObjectBuilder / ListBuilder), the capability runtime (cap), the RPC call envelope (rpc), and the schema compiler (cmd/zapgen). It is not a network library — listeners, transports, and handshakes live downstream; the rpc package defines the call envelope (the bytes), and the generated client takes a channel you supply.

Installation

go get github.com/zap-proto/[email protected]

The schema compiler installs as a separate binary:

go install github.com/zap-proto/go/cmd/[email protected]

Round-trip

Build a message, then parse it back zero-copy. StartObject lays out a struct's fixed section; Parse validates the header and Root() returns an Object that reads fields straight out of the buffer — no decode step, no allocation.

package main

import (
	"fmt"

	zap "github.com/zap-proto/go"
)

func main() {
	// Write: a struct with a uint32 at offset 0 and a text field at offset 8.
	b := zap.NewBuilder(256)
	ob := b.StartObject(16) // 16-byte fixed section
	ob.SetUint32(0, 0xDEADBEEF)
	ob.SetText(8, "zap")
	ob.FinishAsRoot()
	buf := b.Finish() // []byte — the ZAP wire message

	// Read: zero-copy. The Message aliases buf; nothing is deserialized.
	msg, err := zap.Parse(buf)
	if err != nil {
		panic(err)
	}
	root := msg.Root()
	fmt.Printf("%#x %q\n", root.Uint32(0), root.Text(8)) // 0xdeadbeef "zap"
}

Field offsets are explicit here to show the runtime, but in real code you don't write them by hand — zapgen generates a typed View + Builder per struct from a .zap schema (see Codegen below). The header is a fixed 16 bytes (ZAP\x00 magic, version, flags, root offset, size); NewBuilder emits version 1, and Parse accepts version 1 and 2.

Codegen

zapgen reads a .zap schema and emits, per struct, a zero-copy View + Input/New builder, and per interface, a typed RPC client, an abstract ordinal-dispatch server, and a 1-based method-ordinal table over the rpc envelope. Both schema forms — brace and whitespace-significant — go through one parser.

zapgen schema.zap            # emit one <struct>_zap.go per struct, next to the input
zapgen -out ./gen schema.zap # emit into the given directory
zapgen -single schema.zap    # emit one combined <schema>_zap.go

Drop a directive at the top of the consuming package and run go generate ./...:

//go:generate zapgen schema.zap

For this schema:

struct Ping
    Seq u64

struct Pong
    Seq u64

interface Echo
    ping(req: Ping) returns (resp: Pong)
    notify(req: Ping)
    health() returns (resp: Pong)
    shutdown()

zapgen emits NewPing(PingInput{Seq: 41}) (build → []byte) and WrapPing(b) (parse → typed Ping view with a Seq() getter), plus the Echo client/server described under RPC. Method ordinals auto-assign 1, 2, 3, … in declaration order, so appending a method never renumbers the existing ones — wire compatibility is preserved.

RPC

An interface becomes a typed client and an abstract server, carried over the wire by the fixed call envelope in package rpc. The envelope is transport-agnostic: the generated EchoClient takes an EchoChannel you supply (your read/write loop), and the generated DispatchEcho routes a decoded envelope to your handler. Sockets and framing stay downstream.

Server

Implement the generated handler interface; DispatchEcho does the envelope decode and ordinal routing.

type echoHandler struct{}

func (h *echoHandler) Ping(req []byte) ([]byte, error) {
	p, err := WrapPing(req)
	if err != nil {
		return nil, err
	}
	return NewPong(PongInput{Seq: p.Seq() + 1}), nil
}

func (h *echoHandler) Notify(req []byte) error   { return nil }
func (h *echoHandler) Health() ([]byte, error)   { return NewPong(PongInput{Seq: 1}), nil }
func (h *echoHandler) Shutdown() error           { return nil }

// In your read loop, for each inbound envelope:
//   respBytes, err := DispatchEcho(handler, envelope)
// An unknown ordinal yields a StatusNotFound response; a handler error yields
// StatusInternal — both as valid response envelopes, not transport errors.

Client

NewEchoClient takes the channel and an optional capability token attached to every request. Each method builds a request envelope, sends it over the channel, and checks the response status. A request method returns three values: an rpc.Promise (for pipelining), the response body, and an error.

client := NewEchoClient(channel, capToken) // capToken may be nil
_, body, err := client.Ping(NewPing(PingInput{Seq: 41}))
if err != nil {
	return err
}
pong, err := WrapPong(body)
if err != nil {
	return err
}
fmt.Println(pong.Seq()) // 42

The leading rpc.Promise lets you pipeline a dependent call before the first result returns: each method has an On variant (PingOn(promise)) that targets a prior call's promise, collapsing two round trips into one.

EchoChannel is the one interface you implement to bind a transport:

type EchoChannel interface {
	Call(envelope []byte) (rpc.Response, error)
}

Driving the envelope directly

Generated clients are the normal path, but rpc is a public API. Build a request, ship the bytes over any transport, decode the response:

import "github.com/zap-proto/go/rpc"

reqBytes := rpc.BuildRequest(rpc.Call{
	Method:  1,            // the interface method ordinal
	Target:  rpc.NoTarget, // not pipelining off an earlier promise
	Cap:     capToken,     // capability bytes, or nil
	Payload: req,          // the request struct's bytes
})

// ... send reqBytes, receive respBytes ...

resp, err := rpc.ParseResponse(respBytes)
if err != nil {
	return err
}
if resp.Status == rpc.StatusOK {
	pong, _ := WrapPong(resp.Body) // the response struct, zero-copy
	_ = pong
}

Status codes are StatusOK (200), StatusBadRequest (400), StatusUnauthorized (401), StatusForbidden (403), StatusNotFound (404), and StatusInternal (500).

Capabilities

Package cap is the ZAP capability runtime. A Cap is a signed, attenuable token of authority over a Target, granted to a Holder by an Issuer. Caps form a chain via the Parent field; VerifyChain walks back to a root checking each signature, expiry, revocation, target invariance, and monotonically-narrowing permissions.

import "github.com/zap-proto/go/cap"

// Mint a root cap. Ed25519Signer is the built-in mandatory scheme; wire a
// SchemeVerify hook + matching Signer for ML-DSA-65 / hybrid / secp256k1.
signer, _ := cap.NewEd25519Signer()

var target, holder [32]byte // 32-byte content hashes of the resource + holder
c, err := cap.Issue(cap.Issuance{
	Kind:        uint32(cap.KindIAMSession),
	Target:      target,
	Holder:      holder,
	Permissions: cap.PermAttenuate, // bits the holder may exercise / delegate
	ExpiresAt:   2_000_000_000,     // unix seconds; 0 = never
}, signer)
if err != nil {
	return err
}

// Verify a single cap. The Verifier holds the policy: revocation lookup,
// issuer-key resolution, and (optionally) a PQ signature hook.
v := cap.Verifier{
	IssuerKey: func(issuer [32]byte) ([]byte, error) {
		if issuer == signer.Public() {
			return signer.PublicKey(), nil
		}
		return nil, cap.ErrIssuerUnknown
	},
}
if err := v.Verify(c, 1_700_000_000); err != nil { // now, in unix seconds
	return err // ErrExpired, ErrRevoked, ErrSigMismatch, ErrIssuerUnknown, …
}

Derive a narrower child with Attenuate — the signer must hold the parent's holder key, permissions intersect with the parent's, expiry can only shrink, and the parent must carry PermAttenuate (or be a KindDelegate cap):

child, err := cap.Attenuate(c, childHolder, cap.PermAudit, nil, 0, signer)

Signature scope is the SPEC §3 canonical bytes (cap.CanonicalBytes), and CapID = SHA-256(CanonicalBytes ‖ Sig) (Cap.ID), so cap identifiers are byte-identical across every language runtime. Revoke / VerifyRevocation handle revocation; VerifyChain(leaf, chain, op, target, holder, now) validates a full chain including the op-against-mask and delegation gate.

Concurrency

Parse aliases the input buffer and Object is a value type holding a pointer to the immutable message, so reads are safe to share across goroutines. A Builder is single-owner — build per goroutine, not shared.

Build & test

go build ./...
go test ./...

Examples

  • examples/echo — a generated Echo RPC service with an in-memory client/server round-trip
  • cap/cap_test.go — issue, attenuate, chain walk, revocation, caveats

On this page