mirror of
https://github.com/zap-proto/dev.git
synced 2026-10-30 15:05:18 +00:00
Red-team review (this session) caught half the protocol pages were AI
slop — describing Go APIs that don't exist in repos that contain only
schemas, plus performance tables contradicted by the only real bench
harness in tree. This is the surgery:
post-quantum:
- Drop the Z-Wing brand from public docs (user feedback: confusing).
- Reframe PQ as ZAP's stack: IETF X-Wing KEM (transport) + hybrid
Ed25519+ML-DSA-65 (identity) + PQ-RNS (name service). One canonical
story; layers are independent.
- Per-language status matrix shows realistic state (X-Wing KAT shipped
in 4 SDKs, sigs in all, resolver in progress).
protocols/{a2a,acp,fix,ws}:
- Marked "Status — spec-only, January 2026". Schema exists; runtime
doesn't. Removed every Go code snippet that called a non-existent
function. Replaced with a roadmap section.
protocols/rns:
- Rewrote against the actual schema at zap-proto/rns/schema/zap_rns.zap
(Record / Query / Response / signed-by-registry).
- Added the PQ-RNS end-to-end flow diagram and the DID definition
(base32(SHA3-256(kemPk || sigPk))).
- Documents the cross-language KAT fixture published at
zap-proto/rns/testdata/pqrns_kat.json.
protocols/mcp:
- Rewrote against the actual bridge.go API: NewBridge(*zap.Node),
AddServer, GetTools, CallTool, CallToolByName. Bridges existing
stdio MCP servers (it's a bridge, not a from-scratch impl).
- Dropped the invented zmcp.NewServer / Connect / ListTools API.
protocols/http:
- Killed the 6× req/s / 19× p99 / 6.2× RSS table — contradicted by
bench-results.txt which shows ZAP-HTTP currently ~10% slower.
- Replaced with honest current numbers + the value-prop statement.
protocols/native:
- Killed the 18× / 38× / 6.7× / 9.2× gRPC table — the bench harness
has no gRPC comparator, the numbers were fabricated.
- Replaced with the actual concurrency=32 numbers from bench-results
+ an explanation that the win is semantic, not currently latency.
transports:
- Killed the @zap-proto/wasm and Cloudflare-Worker code snippets that
referenced a non-existent npm package.
- Replaced with the honest pattern: browser→TLS→Worker→ZAP-over-X-Wing
to origin. Points readers at the SDK status table.
app/page.tsx:
- Dropped Z-Wing from the home page cards.
240 lines
7.5 KiB
Plaintext
240 lines
7.5 KiB
Plaintext
---
|
||
title: Native ZAP RPC
|
||
description: ZAP used directly — capability-based RPC, no higher-level protocol on top.
|
||
---
|
||
|
||
# Native ZAP RPC
|
||
|
||
When you reach for HTTP, MCP, or FIX, you're carrying *someone else's* protocol on the ZAP wire. **Native ZAP RPC** is when ZAP is the application protocol: you define your own interfaces in `.zap` schema, generate code, and your services talk directly in ZAP messages.
|
||
|
||
This is the fastest path on the wire and the only path that gives you ZAP's full capability model — explicit, transferable, revocable capabilities instead of bearer tokens.
|
||
|
||
## When to use native vs. a higher-level protocol
|
||
|
||
| Use native ZAP RPC | Use a protocol-over-ZAP |
|
||
|---|---|
|
||
| Greenfield internal service ↔ service | You need to be reachable from existing HTTP / MCP / FIX clients |
|
||
| You want capabilities, not API keys | You're integrating with code you don't control |
|
||
| You want streaming with no framing tax | You're behind a corporate gateway that only knows HTTP |
|
||
| You care about every µs | The classical protocol's semantics are exactly right |
|
||
|
||
## Define a schema
|
||
|
||
```
|
||
# calculator.zap
|
||
interface Calculator
|
||
add (a Float64, b Float64) -> (result Float64)
|
||
subtract (a Float64, b Float64) -> (result Float64)
|
||
divide (a Float64, b Float64) -> (result Float64)
|
||
sum (numbers List(Float64)) -> (total Float64)
|
||
observe (subject Text) -> (stream stream Float64)
|
||
```
|
||
|
||
No file IDs. No ordinals. Forward-compatible: append-only field/method addition is non-breaking by construction.
|
||
|
||
## Generate code
|
||
|
||
```bash
|
||
zap compile -o ./gen calculator.zap --go --ts --py --rust --java
|
||
```
|
||
|
||
One pass generates every language at once. The compiler is content-addressed; an unchanged schema produces byte-identical output.
|
||
|
||
## Server (Go)
|
||
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"context"
|
||
"errors"
|
||
"log"
|
||
|
||
zap "github.com/zap-proto/go"
|
||
"yourmod/gen"
|
||
)
|
||
|
||
type calc struct{}
|
||
|
||
func (calc) Add(ctx context.Context, a, b float64) (float64, error) { return a + b, nil }
|
||
func (calc) Subtract(ctx context.Context, a, b float64) (float64, error) { return a - b, nil }
|
||
|
||
func (calc) Divide(ctx context.Context, a, b float64) (float64, error) {
|
||
if b == 0 {
|
||
return 0, errors.New("division by zero")
|
||
}
|
||
return a / b, nil
|
||
}
|
||
|
||
func (calc) Sum(ctx context.Context, ns []float64) (float64, error) {
|
||
var t float64
|
||
for _, n := range ns {
|
||
t += n
|
||
}
|
||
return t, nil
|
||
}
|
||
|
||
func (calc) Observe(ctx context.Context, subject string, out zap.Stream[float64]) error {
|
||
ticker := time.NewTicker(time.Second)
|
||
defer ticker.Stop()
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
case <-ticker.C:
|
||
if err := out.Send(rand.Float64()); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func main() {
|
||
srv := zap.NewServer(zap.WithAddr("zap://0.0.0.0:9000"))
|
||
gen.RegisterCalculatorServer(srv, calc{})
|
||
log.Fatal(srv.ListenAndServe())
|
||
}
|
||
```
|
||
|
||
Every parameter and return is a zero-copy ZAP buffer. `zap.Stream[T]` is the bidirectional half of an open call — `Observe` can push values until the client disconnects.
|
||
|
||
## Client (Go)
|
||
|
||
```go
|
||
ctx := context.Background()
|
||
|
||
client, _ := zap.Dial(ctx, "zap://calc.example:9000")
|
||
defer client.Close()
|
||
|
||
c := gen.NewCalculatorClient(client)
|
||
|
||
sum, _ := c.Add(ctx, 10, 5)
|
||
fmt.Println(sum) // 15
|
||
|
||
stream, _ := c.Observe(ctx, "ticker")
|
||
for v := range stream.Recv(ctx) {
|
||
fmt.Println(v)
|
||
}
|
||
```
|
||
|
||
## Capabilities
|
||
|
||
Native ZAP RPC's killer feature: methods can return **capabilities** — references that the holder can call but cannot forge, transfer freely, and the issuer can revoke without changing keys.
|
||
|
||
```
|
||
# storage.zap
|
||
interface Storage
|
||
open (path Text) -> (file File)
|
||
|
||
interface File
|
||
read (n UInt32) -> (bytes Data)
|
||
write (bytes Data) -> ()
|
||
close () -> ()
|
||
```
|
||
|
||
```go
|
||
// Server: returning a capability
|
||
func (s *storage) Open(ctx context.Context, path string) (gen.File, error) {
|
||
f, err := os.Open(path)
|
||
if err != nil { return nil, err }
|
||
return &fileHandle{f: f}, nil
|
||
}
|
||
```
|
||
|
||
```go
|
||
// Client: using a returned capability
|
||
storage, _ := gen.NewStorageClient(client).Open(ctx, "/etc/hosts")
|
||
data, _ := storage.Read(ctx, 1024)
|
||
storage.Close(ctx)
|
||
```
|
||
|
||
Crucially, you can pass `storage` to a *different* peer over the same connection — they can read from `/etc/hosts` on the original server without ever holding the original client's identity. That's capability delegation. There is no equivalent in HTTP.
|
||
|
||
## Multi-language clients
|
||
|
||
The exact same schema generates working clients in every language. From a TypeScript client calling the Go server above:
|
||
|
||
```ts
|
||
import { Calculator } from './gen/calculator'
|
||
import { connect } from '@zap-proto/zap'
|
||
|
||
const client = await connect('zap://calc.example:9000')
|
||
const calc = new Calculator(client)
|
||
|
||
console.log(await calc.add(10, 5)) // 15
|
||
for await (const v of calc.observe('ticker')) {
|
||
console.log(v)
|
||
}
|
||
```
|
||
|
||
Python:
|
||
|
||
```python
|
||
from gen.calculator import Calculator
|
||
from zap_proto import connect
|
||
|
||
async with await connect('zap://calc.example:9000') as client:
|
||
calc = Calculator(client)
|
||
print(await calc.add(10, 5)) # 15
|
||
async for v in calc.observe('ticker'):
|
||
print(v)
|
||
```
|
||
|
||
Rust:
|
||
|
||
```rust
|
||
use zap_proto::Client;
|
||
use gen::calculator::CalculatorClient;
|
||
|
||
#[tokio::main]
|
||
async fn main() -> anyhow::Result<()> {
|
||
let client = Client::connect("zap://calc.example:9000").await?;
|
||
let calc = CalculatorClient::new(client);
|
||
|
||
println!("{}", calc.add(10.0, 5.0).await?); // 15
|
||
let mut stream = calc.observe("ticker").await?;
|
||
while let Some(v) = stream.next().await {
|
||
println!("{}", v?);
|
||
}
|
||
Ok(())
|
||
}
|
||
```
|
||
|
||
## Authentication
|
||
|
||
Connections are mutually KEM-authenticated by default. The peer's identity is bound to its keypair, not to a bearer token:
|
||
|
||
```go
|
||
srv := zap.NewServer(
|
||
zap.WithAddr("zap://0.0.0.0:9000"),
|
||
zap.WithIdentity(zap.LoadIdentity("./service.key")),
|
||
zap.WithAuthorize(func(peer zap.Peer, method string) error {
|
||
if peer.DID() != "did:zap:z6Mk..." && method == "Divide" {
|
||
return zap.ErrPermissionDenied
|
||
}
|
||
return nil
|
||
}),
|
||
)
|
||
```
|
||
|
||
For unauthenticated services (public read-only endpoints), use `zap.WithAnonymous()`.
|
||
|
||
## Performance
|
||
|
||
The current [zap-proto/bench](https://github.com/zap-proto/bench) harness measures ZAP-HTTP and a length-prefixed native ZAP echo against `net/http`+JSON. Honest numbers from `bench-results.txt` (concurrency=32, small workload):
|
||
|
||
- HTTP/1.1+JSON: 34,411 req/s
|
||
- ZAP-binary HTTP: 30,868 req/s (0.90×)
|
||
|
||
A native-ZAP echo (no HTTP shape, no headers, length-prefixed frames) is in `native_zap_test.go` — comparison numbers will be folded into `bench-results.txt` in the next harness pass. Until then there is no published native-vs-gRPC table here, because we don't have a reproducible gRPC comparator in-tree.
|
||
|
||
What native ZAP RPC actually buys you over gRPC isn't µs of latency — that depends on a careful harness — it's the semantic shift: **capabilities instead of bearer tokens**, **identity-bound transport** instead of TLS-over-PKI, **one wire format** that the rest of the protocol stack rides on. The latency improvement, when measured, will land in the benchmarks page sourced from the bench harness output.
|
||
|
||
## Related
|
||
|
||
- [Protocol wire spec](/docs/protocol) — byte-for-byte format
|
||
- [Architecture](/docs/architecture) — why capabilities, why content-addressed
|
||
- [Transports](/docs/transports) — TCP, UDP+FEC, shared-memory, in-process
|
||
- [Gateway](/docs/gateway) — translate native ZAP into HTTP/JSON for browsers
|
||
- [SDKs](/docs/sdks) — per-language client + server libraries
|