mirror of
https://github.com/zap-proto/dev.git
synced 2026-10-30 15:05:18 +00:00
Replace the fictional MCP-style tool-calling API (Zap.connect / call_tool / Gateway / ToolCall) and wrong package names with the verified zero-copy wire + capability RPC surface, every name checked against source. - go.mdx: github.com/zap-proto/[email protected]. Builder→Parse round-trip, zapgen codegen (NewPing/WrapPing/PingInput), the rpc call envelope (BuildRequest/ParseResponse + 3-value pipelining client), and the cap layer (Issue/Attenuate/Verifier.Verify). Compiled+ran a generated Calculator client/server end to end (prints 15). - python.mdx: PyPI zap-proto, import zap (not zap_protocol). Wire round-trip, zap.rpc (Server/Client/Method — ran end to end), zap.cap (issue/attenuate/ Verifier — ran issue+verify), and the [app] decorator app (ZAP + Client). - rust.mdx: zap-schema crate (zapc bin) + the zap-proto/rust workspace crates (zap/zapc/zap-rpc/zap-futures). Real zap-rpc Server trait + RpcSystem client + zap::cap (issue/Verifier). Drops nonexistent zap-proto/zap-codegen crates, spec-rs, zap_protocol module. - cpp.mdx: separates the cpp-core schema front-end + zero-copy zap:: runtime (MallocMessageBuilder / messageToFlatArray / TwoPartyClient) from the cpp high-level binding; drops the fictional zap::connect/callTool client. - quick-start.mdx: real install/codegen (zapgen, cargo install zap-schema, @zap-proto/zapc — not zap-codegen / @zap-proto/codegen / spec/cmd/zap) and real generated-RPC server/client per language.
285 lines
9.1 KiB
Plaintext
285 lines
9.1 KiB
Plaintext
---
|
|
title: Go
|
|
description: ZAP Go SDK — the canonical zero-copy wire runtime, capability layer, and zapgen RPC codegen.
|
|
---
|
|
|
|
# Go Binding
|
|
|
|
[`github.com/zap-proto/go`](https://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
|
|
|
|
```bash
|
|
go get github.com/zap-proto/[email protected]
|
|
```
|
|
|
|
The schema compiler installs as a separate binary:
|
|
|
|
```bash
|
|
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.
|
|
|
|
```go
|
|
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](#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.
|
|
|
|
```bash
|
|
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
|
|
//go:generate zapgen schema.zap
|
|
```
|
|
|
|
For this schema:
|
|
|
|
```zap
|
|
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](#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.
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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.
|
|
|
|
```go
|
|
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):
|
|
|
|
```go
|
|
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
|
|
|
|
```bash
|
|
go build ./...
|
|
go test ./...
|
|
```
|
|
|
|
## Examples
|
|
|
|
- [examples/echo](https://github.com/zap-proto/go/tree/main/examples/echo) — a generated Echo RPC service with an in-memory client/server round-trip
|
|
- [cap/cap_test.go](https://github.com/zap-proto/go/blob/main/cap/cap_test.go) — issue, attenuate, chain walk, revocation, caveats
|