Files
zeekayandHanzo Dev dc09e5ff0b zap: give Rust a real client, and stop two codebases sharing one crate name
The Rust side was server-only and could not read a single Go RPC
envelope: `Header::decode` and `Message::parse` accepted wire version 1,
and every `zap-proto/go` envelope is version 2 (`NewBuilderV2`). It also
declared the crate name `hanzo-zap`, which a DIFFERENT codebase
(hanzo/net/hanzo-zap) already publishes at 0.6.75 with real consumers.

Crate identity
  hanzo-zap -> zap-proto, imported as `zap` — the same split the Go
  module has (github.com/zap-proto/go, package zap). crates.io
  zap-proto was ours at 1.0.0 (a reservation), so 1.0.1 is latest+1
  patch. `hanzo-zap` stays with hanzo/net, which published its max
  version and has the pins.

Wire
  wire: accept version 1 AND 2 (VERSION2); drop write_frame_async.
  codec: the data segment — a line-for-line port of Go's zap.go +
  builder.go, including its adversarial bounds rules.
  rpc: the 28-byte call envelope, the 20-byte answer, and promise
  pipelining. Pipeliner is now a Dispatch that PARKS a dependent whose
  target has not resolved and answers it when it does — no drain()
  side channel, which was a second way to get a response out.
  transport: Conn::connect / Server::listen over TCP or a unix socket
  (the address shape picks the socket, as in zip), [u32 len][dir]
  [envelope] framing identical to Go's, PromiseID correlation, bounded
  in-flight dispatch. The connection is symmetric, so a server holding
  its accepted Conn can CALL the client — the push path that a strict
  request/response handler cannot have.

  Deleted server.rs: it framed raw ZAP messages with no direction tag,
  a second incompatible wire no Go peer speaks. Deleted the echo
  daemon binary with it.

Proof
  tests/crosswire.rs — golden hex regenerated from zap-proto/[email protected]'s
  own BuildRequest/BuildResponse; Rust must parse AND emit those bytes.
  tests/interop.rs — builds interop/main.go (a real transport.Listen
  server) and drives it from Conn over a unix socket. Reverting the one
  version condition turns both red with InvalidVersion(2).
  make test is the gate and .github/workflows/test.yml runs it, with Go
  installed so the interop test can never silently skip.

Session crypto is still absent: local-over-unix-socket is correct,
remote stays on HTTPS. README documents what closing it takes.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 22:52:13 -07:00

57 lines
1.3 KiB
Go

// A live Go ZAP peer, so the Rust runtime is proved against the REAL
// reference implementation rather than a hand-written fixture.
//
// go run ./interop /path/to/zap.sock
//
// It serves one method — ordinal 1, "upper" — over the canonical
// transport framing, prints "ready" once bound, and runs until killed.
// rust/tests/interop.rs drives it.
package main
import (
"fmt"
"os"
"os/signal"
"strings"
"syscall"
"github.com/zap-proto/go/rpc"
"github.com/zap-proto/go/transport"
)
func main() {
if len(os.Args) < 2 {
fmt.Fprintln(os.Stderr, "usage: interop <addr>")
os.Exit(2)
}
addr := os.Args[1]
network := "tcp"
if strings.HasPrefix(addr, "/") || strings.HasPrefix(addr, "./") {
network = "unix"
}
srv, err := transport.Listen(network, addr, func(env []byte) ([]byte, error) {
call, err := rpc.ParseRequest(env)
if err != nil {
return nil, err
}
if call.Method != 1 {
return rpc.BuildResponse(rpc.StatusNotFound, call.PromiseID, nil), nil
}
up := strings.ToUpper(string(call.Payload))
return rpc.BuildResponse(rpc.StatusOK, call.PromiseID, []byte(up)), nil
})
if err != nil {
fmt.Fprintln(os.Stderr, "listen:", err)
os.Exit(1)
}
defer srv.Close()
fmt.Println("ready", srv.Addr().String())
os.Stdout.Sync()
sig := make(chan os.Signal, 1)
signal.Notify(sig, os.Interrupt, syscall.SIGTERM)
<-sig
}