Files
zeekay 6c4cf2383f fix(schema): make every name the schema can spell reachable
Differential fuzzing against Go's zapgen turned up three names the emitter
could not carry. A field called wrap or build was refused outright; a field
called from generated a keyword; two fields answering to one name generated a
duplicate argument, which is a SyntaxError rather than a diagnosis.

Now one rule covers all of them: a name Python or the view already claims
takes the trailing underscore PEP 8 prescribes, so `None` — a real field name
in the fleet — reads back as None_. Two fields that land on the same name are
refused by name, the one place this reader is stricter than the reference,
which accepts that schema and writes Go that will not compile.
2026-09-15 11:22:12 -07:00

6.4 KiB

zap-proto (Python) — ZAP zero-copy app proto

Python ZAP implementation. Full docs in README.md. PyPI dist name zap-proto, import zap. The core is pure stdlib — import zap pulls no third-party dep.

src/zap/:

  • wire — the zero-copy codec. Pure-stdlib port of the canonical Go runtime (zap-proto/go: zap.go + builder.go + accessors.go + list_cursor.go). Message/Object/List reads + Builder/ObjectBuilder/ListBuilder writes; 16-byte ZAP\x00 header; Version1+2 accept; reader hardening (reject backward pointers into the header, clamp list length). Byte-faithful to Go — pinned by the shared golden vector in tests/test_wire.py.
  • rpc — real TCP request/response transport. Server dispatches the Zap interface method ordinals (Method enum, @0..@8 from zap.zap); Client calls them. JSON body, magic-framed envelope (reuses protocol).
  • pipeline — promise pipelining (the ONE canonical ZAP model), the faithful Python peer of Go's zap-proto/go/rpc (Session + Pipeliner) and TS @zap-proto/zap promise.ts. Session (client: monotonic promise_id allocator starting at 1 + origin/pipeline Call builders that set target to NO_TARGET vs a prior promise's id) + Pipeliner (server: per-connection promise table that records each OK answer's Body under its promise_id, substitutes a resolved target's Body for a dependent's Payload before dispatch, QUEUES a dependent whose target is unresolved until it resolves, REFUSES — STATUS_BAD_REQUEST — a dependent whose target answered non-OK or was finished; never hangs). finish(id) bounds the table with a terminal "finished" state that refuses + wakes parked dependents. Operates on the binary Call/Response envelope built with zap.wire (build_request/build_response/parse_request/parse_response, target @8) — byte-for-byte identical to Go rpc.BuildRequest/BuildResponse and TS buildRequest/buildResponse (pinned by tests/test_pipeline.py, which asserts equality with golden Go hexes and which were verified to round-trip through the real Go ParseRequest/ParseResponse). NOTE: this is the SAME wire as Go/TS pipelining and is DISTINCT from rpc.py's JSON {id,method,params} transport envelope — pipelining shares the byte-level wire with the other runtimes; the rpc.Server/Client JSON transport does not. Pure stdlib.
  • frame — the zapd router-envelope codec, byte-compatible with zap-proto/zapd/src/frame.rs + ZapClient (connect / hello / providers.list / route). Used by hanzo-tools-browser's zapd_consumer.
  • protocol — magic-framed JSON framing (ZAP\x01 + type + length + JSON); decode_stream demuxes back-to-back frames.
  • app — FastMCP-style decorator app (ZAP); run() serves real rpc. Needs the [app] extra (pydantic). Lazy-imported from zap/__init__.py.
  • client — Client (sync RPC over rpc) + ZapClient (router UDS).
  • crypto — real ML-KEM-768 / ML-DSA-65 (pqcrypto) + X25519 (cryptography), hybrid HKDF-SHA256 KEX. Needs the [crypto] extra. Hard-fails with CryptoError when a backend is missing — never fabricates bytes.
  • cap — the capability runtime (signed, attenuable authority tokens). Faithful port of zap-proto/go/cap over the capabilities.zap v1.1 schema. wrap, Cap view, canonical_bytes (SPEC §3 signed scope — byte-identical to Go), id = SHA-256(canonical_bytes ‖ Sig) (SPEC §4). Issuance/issue/ attenuate enforce the SPEC §2.3 delegation gate at MINT time (parent needs PERM_ATTENUATE or CapKind.DELEGATE), monotonic permission narrowing, holder→issuer linkage, and downward-only expiry. Verifier.verify / verify_chain enforce the full §2.3 invariants with FAIL-CLOSED scheme dispatch (reserved tag 0x00 and unknown tags refused — never downgraded). Revocation/revoke/verify_revocation (scheme-aware). Schemes: Ed25519 (mandatory bootstrap), ML-DSA-65 (FIPS 204), secp256k1 ECDSA — all real and verified; a missing backend raises SchemeUnavailable (an UnhandledSchemeError, so fail-closed). Wire/canonical/CapID paths are pure stdlib; signing needs the [crypto] extra. Cross-language interop is pinned by tests/testdata/cap_go_kat.{hex,json} — a Go-signed cap that decodes, recomputes the same canonical bytes + CapID, and verifies in Python (and a deterministic Python re-issue is byte-identical to Go).
  • identity — W3C DID (did:zap/did:key/did:web). consensus — voting.
  • schema — native .zap schema support: read a schema, generate Python bindings. python -m zap.schema SCHEMA.zap writes one <schema>_zap.py. The schema is whitespace-significant (indentation, no braces), and reading it is the SAME two steps as zap-proto/go's cmd/zapgen: desugar rewrites the whitespace form into the canonical brace form — a near-identity that only ADDS the {/} and @offset tokens the brace grammar requires, so a pure-brace file round-trips byte-for-byte — then the brace parser runs unchanged. Two styles, one grammar, and both runtimes read a schema the same way. Emitted bindings are zero-copy views over zap.wire (wrap parses, properties read fields IN PLACE at their schema offsets, build writes) plus, per interface, an ordinal IntEnum, a handler Protocol, a dispatch_*, and a typed client over zap.pipeline (Channel / CallFailed live there, so a generated module declares only the schema's own names). Pinned against Go two ways in tests/test_schema.py: the desugar golden corpus is cmd/zapgen/desugar_test.go verbatim, and Ping.build / BaseTx.build must reproduce the bytes the GO generator's own output writes. Measured on the 263 unique .zap files in the estate: the two generators AGREE on accept-or-reject for all 263 (135 accept, 128 reject, same message), and every accepted schema's generated module imports, is ruff-clean and passes mypy --strict. A name Python or the view already claims (from, None, wrap, build) takes the trailing underscore PEP 8 prescribes; two fields answering to one name is refused, the one place this reader is stricter than Go, which accepts that schema and writes Go that does not compile. Pure stdlib.

Gates (CI in .github/workflows/ci.yml): ruff check, ruff format --check, mypy --strict src/zap/, pytest — all green. Use uv for the venv.