Files
zip/caller_linux.go
zeekayandHanzo Dev d15ad939f6 feat(call): services call services by op name, over ZAP on a unix socket
The typed-op registry gets its fourth projection. An aggregator that reaches
another app today either imports its package — dragging in that app's whole
dependency graph, which is what turns a fleet of small services into one
mega-binary — or hand-writes a client against its URLs, which is the schema
copied into a second place to drift. Neither is necessary: the op already has
a stable name, and now that name is callable.

    c, err := zip.DialApp("flags")               // $ZIP_RUNTIME_DIR/flags.sock
    out, err := zip.Call[BoolIn, BoolOut](ctx, c, "flags_bool", &in)

The plane is an ordinary route (/.well-known/zip/op/:op), so it rides every
transport Listen was given and inherits the app's middleware, error handler,
validator and Authorizer — exposed exactly as much as the REST routes and no
more. It resolves through opByName + op.invoke, the same pair tools/call runs,
without MCP's agent-facing envelope. Errors cross whole: the wire form is what
errorHandler already writes, so errors.As sees the *HTTPError the callee
returned, status intact. A void op yields (nil, nil).

Generic, not generated, deliberately: a generated per-op Go client would put a
second copy of the schema in the caller's repo on its own regeneration
schedule — the drift this plane removes, one directory over — and would be a
fourth client generator racing openapi-generator, which already builds the
published SDKs from the OpenAPI projection.

ONE socket-path scheme, used by both halves so they cannot drift:

    SocketPath(name) = RuntimeDir() + "/" + name + ".sock"
    RuntimeDir()     = $ZIP_RUNTIME_DIR | $XDG_RUNTIME_DIR/zip | /run/zip

A service serves at zip.Addr(zip.SocketPath("flags")), a caller reaches it with
zip.DialApp("flags"). socketIn states the shape once; the plugin loader names a
child's private socket with the same function. Listen creates the directory
0700 when missing, so the canonical path works on a fresh host.

Identity is never forged, because the caller never states it. WHO the request
is for is the gateway's assertion, forwarded by Ctx.Forward and read by
CallerOf; WHAT is calling is SO_PEERCRED, read by PeerOf — the peer does not
send it, the kernel answers on its behalf. nil means "cannot attest", so a
check fails closed rather than reading a zero Peer as root.

MCPTools() exposes the tool surface the same way OpenAPISpec() exposes the
document, so a host can compose plugins' tools in-process without a round trip.

openapi.go now reads operationId from opName, the one place that rule lives,
instead of re-deriving it inline — all four projections address an op by the
same token.

Cost: Benchmark_TypedRoute/typed 28 -> 29 allocs/op (the one context value that
makes CallerOf and PeerOf possible). The untyped path is unchanged at 27 and
still pinned at one heap value per request.

Proven end to end: a real plugin on a unix socket, a client dialing it, a typed
round trip with no HTTP in the path — the reply frame is decoded with zaphttp's
own codec, HTTP/1.1 text gets no HTTP answer, and the peer credential the
handler observes is proof the transport was unix. Full suite green with
CGO_ENABLED=0 and under -race.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-28 12:56:05 -07:00

35 lines
965 B
Go

//go:build linux
package zip
import (
"net"
"syscall"
)
// peerOf reads SO_PEERCRED off a unix connection — the pid, uid and gid the
// KERNEL recorded for the process at the far end when it connected. It is not
// a value the peer sends, so there is nothing for it to forge.
//
// A non-unix conn has no answer: a tcp peer is an address, and an address is
// not an identity. That case returns nil rather than a zero Peer, so "not
// attested" is never mistaken for "attested as root".
func peerOf(c net.Conn) *Peer {
uc, ok := c.(*net.UnixConn)
if !ok {
return nil
}
raw, err := uc.SyscallConn()
if err != nil {
return nil
}
var cred *syscall.Ucred
var sockErr error
if err := raw.Control(func(fd uintptr) {
cred, sockErr = syscall.GetsockoptUcred(int(fd), syscall.SOL_SOCKET, syscall.SO_PEERCRED)
}); err != nil || sockErr != nil || cred == nil {
return nil
}
return &Peer{PID: int(cred.Pid), UID: int(cred.Uid), GID: int(cred.Gid)}
}