Skip to main content

taut — one declarative IR -> native types, deterministic CBOR wire, IR-driven codec + service contracts, verified by a golden conformance corpus (import: `taut`)

Project description

taut

One tiny declarative contract → native types, a deterministic wire codec, and service stubs across Python, TypeScript, Rust, C++, Swift, Go, Kotlin, JS, and Java — verified byte-for-byte by a shared golden corpus.

taut is a cross-language data + service protocol mechanism: protobuf's essential value (a single governed schema, generated types, a portable wire format, an evolution gate) with roughly 90% less of its weight (no plugin toolchain, no .proto grammar, no required runtime, a wire you can read in an afternoon). The schema is authored as a small, restricted Python DSL — or any .ir.json — and everything else is projected from it.

Status: alpha. The model, codec, generators, corpus, and evolution gate are implemented across nine languages; APIs may still move.

pip install taut-proto      # distribution name (PyPI)
import taut                 # import name
tautc gen api.taut.py -o gen/   # CLI codegen

The distribution is named taut-proto (PyPI rejects the bare taut); the import package and CLI are taut / tautc — the Pillow→PIL pattern. Pure Python, no runtime dependencies.


taut vs proto3

A schema language earns its keep by removing ambiguity between services in different languages. proto3 (with gRPC) does that but drags in a code-gen plugin architecture, a bespoke grammar, per-language runtimes, an HTTP/2 RPC stack, and a wire format few can read by hand. taut keeps the parts that matter and drops the rest:

Dimension proto3 / gRPC taut
Schema source .proto DSL + protoc + a plugin per language a few lines of typed Python, or .ir.json
Toolchain protoc compiler + language plugins one pip install; tautc for compiled targets
Wire format varint tag-length-value; not guaranteed canonical across implementations (maps are explicitly unordered) frozen deterministic CBOR (RFC 8949 §4.2) — byte-identical everywhere, corpus-pinned
Codegen required? yes for most languages (or descriptor-based reflection) no for Python/TS (IR-driven runtime codec); generated for the compiled targets (Rust/C++/Swift/Go/Kotlin/JS/Java)
Runtime dependency a protobuf runtime per language none (stdlib; hand-rolled codec per target)
Field presence scalars had none; optional presence re-added in 3.15 optional → null; presence is clean (fields always emitted)
required removed in proto3 kept as a governance assertion, enforced by the evolution gate (never a decode error)
Forward-compat (unknown fields) dropped in 3.0, restored in 3.5 — and binary only (the JSON mapping drops them) opt-in unknown-field preservation (raw tags re-emitted in canonical order)
Services / RPC service/rpc, but the transport is gRPC: HTTP/2 + codegen + runtime (name, in, out, shape); transport-agnostic (reference: JSON envelope + CBOR payload)
Streaming gRPC client/server/bidi streams first-class delivery shapes: atom / log / stream / swmr / snapshot_delta / crdt
Schema evolution field-number rules + reserved; breaking-change detection is external (buf, protolock) a built-in structural breaking-change gate, runnable in CI
Extensions proto2 had them; proto3 dropped them (use Any) declared, typed side-channels at a reserved tag band
Read the whole spec large surface the IR fits on a screen

Because the IR is data — not a Turing-complete program — taut can diff two versions of a contract and tell you mechanically what broke.

What taut deliberately doesn't have (yet): oneof types, well-known types (Timestamp/Any/Duration), a canonical JSON profile (deferred), descriptor/reflection services, and — above all — proto's ecosystem maturity, scale hardening, and gRPC's battle-tested multi-language RPC runtime. taut is a focused mechanism, not a drop-in protobuf replacement for every use case. If you need proto's breadth, use proto; taut is the lighter contract when you want ~10% of the load for the parts that actually bite.

The contract

A schema is enums + messages + services, authored declaratively:

from taut.ir.dsl import INT, STR, BYTES, Enum, F, Msg, Params, Ref, List, method, service, schema

SCHEMA = schema(
    service("Razel",
        method("build", role="in", params=Params(target=STR), out=Ref.BuildResult),
        method("build.subscribe", role="out", shape="atom", out=Ref.BuildResult)),

    BuildStatus=Enum(cached=0, built=1, failed=2),

    OutputArtifact=Msg(
        path=F(1, STR),
        digest=F(2, BYTES),
        next_id=3),

    BuildResult=Msg(
        target=F(1, STR),
        status=F(2, Ref.BuildStatus),
        outputs=F(3, List(Ref.OutputArtifact)),
        message=F(4, STR, optional=True),
        next_id=5),
)
  • Fields carry an explicit integer tag, a type, and flags (optional, transient, CRDT merge). Types are a closed set: scalars (int/str/bytes/ bool), enum/message refs, List, and Map (a key-sorted, deterministic keyed collection — K scalar, V scalar/enum/message).
  • Keyword-named enums, messages, fields, Ref.Name references, and Params(...) method params are preferred. The explicit string forms remain available for names that are not Python identifiers, Python keywords, or Msg(...) control names such as reserved and next_id.
  • reserved + next_id are first-class, validated message features (not comments) — retired tags/names can never be reused, and next_id is checked.
  • Methods are the minimal contract (name, in, out, shape). shape is the sole discriminator: unary (request→response, the default) is just the degenerate member of an open shape registry, alongside streaming shapes atom (latest-wins state), log (append-only), stream (live), swmr (snapshot+delta), snapshot_delta, and crdt. out binds a type per the shape's delivery slots. There is no separate "kind" axis to disagree — illegal states are unrepresentable. (See Reference §5–6.)

A validator rejects incoherent IR (dangling refs, duplicate tags, out-slots that don't match a shape, …), so downstream mechanism can be derived without review.

The wire

A hand-rolled, frozen deterministic CBOR subset: definite-length, shortest- form integers, ascending integer map keys. The same value encodes to the same bytes in every language — pinned by a golden conformance corpus (value → exact hex). Python and TypeScript run a fully IR-driven codec (instantiate a client or server from JSON alone, zero codegen); Rust, C++, Swift, Go, Kotlin, JS, and Java get generated native types with encoders/decoders (compiled targets need types ahead of time). The C++ corpus is a wall of static_asserts — compiling is the test.

Opinionated about the wire, not the API

Small-and-tight vs. bloated is only half the pitch. The other half: taut pins exactly what cross-language interop requires — the deterministic wire, the (name, in, out, shape) contract, and the evolution gate — and nothing more. Those must be fixed, so they are.

How that contract surfaces in your code is your call. The IR is the governed artifact (a tiny JSON); projecting it into types, clients, and servers is a choice, not a mandate:

  • The bundled tautc generators are reference projections. Use them, swap them, or write your own against the exported .ir.json — pydantic models, plain dataclasses, a transactional/ORM wrapper, different client ergonomics, whatever fits your app.
  • Or generate nothing: Python and TypeScript drive the IR straight through the runtime codec.

Where protoc hands you its message classes and gRPC its stubs, taut hands you the bytes and the schema and gets out of the way.

tautc — the reference codegen CLI

tautc gen IR --out DIR [--lang python,typescript,rust,cpp] [--service A,B] [--api-only]

Loads a .taut.py or .ir.json, validates it, and emits per language: api (native types + encoders/decoders), plus client/server stubs per service. --api-only emits just the struct defs + codec — the drop-in for a build script on a compiled target. --with-runtime additionally emits the vendored CBOR runtime for compiled targets (rustcbor.rs, cpptaut/cbor.hpp), so the generated output compiles standalone — no hand-vendoring. Example: razel (a Rust build daemon) authors ir/razel.taut.py and generates its Rust wire layer with tautc gen ir/razel.taut.py -o razel/gen --lang rust --api-only.

These are the reference emitters; they read the exported .ir.json, and so can a generator you write — tautc is a convenience, not a requirement.

Conformancetautc corpus IR -o DIR [--lang rust] [--check] derives a golden corpus from the IR (deterministic coverage values, one per message, encoded by the canonical codec → golden.json) plus a per-language byte-parity harness (Rust vectors.rs today). A target conforms iff its codec reproduces those bytes; --check is the CI drift gate. No hand-authored vectors.

CBOR ↔ JSONtautc json IR -m MESSAGE converts the deterministic-CBOR wire to/from JSON via the IR (stdin/stdout or -i/-o files; --from-json reverses; --indent pretty-prints). proto3-style conventions (int64→string, bytes→base64, enum→name); cbor → json → cbor is byte-identical. Handy for debugging and JSON-native gateways:

some-producer | tautc json api.taut.py -m BuildResult            # CBOR -> JSON
tautc json api.taut.py -m BuildResult --from-json < x.json       # JSON -> CBOR

Evolution

  • Breaking-change gate — structural diff of two IR versions classifies each change breaking / compatible (remove field, retag, change a method's shape → breaking; add a field/method → compatible). Run it in CI.
  • Forward-compatibility — unknown-field preservation: a decoder keeps unrecognized tags as raw CBOR and re-emits them in canonical order, so an old reader relaying a new message loses nothing. Default-on in the Python/TS runtime codec; opt-in for generated structs (Rust + C++) via tautc gen --forward-compat (a wire_residual field). Proven across all four languages by a cross-version test: a v1 struct round-trips a v2-only field byte-for-byte — even when the unknown tag interleaves between known ones.
  • Extensions — declared, typed side-channels at a reserved tag band (BAND_START = 2^20); infrastructure reads/writes them on the wire without the app schema knowing.

Layout

src/taut/        the builder (pure Python)
  ir/            model, DSL, validator, export/load, breaking-change gate, shapes
  wire/          deterministic CBOR + IR-driven codec
  gen/           generators: Rust, C++, and the per-language scaffold
  cli.py         the `tautc` command
ir/              authored IR modules — the only governed artifact
                 (griplab.taut.py, razel.taut.py)
corpus/          generated golden vectors (the oracle)
docs/            Overview, GettingStarted, Reference, Server + a runnable example
dev-docs/        design notes + the decisions log (Taut*.md)
.github/         the PyPI publish workflow (Trusted Publishing on release)

Reference target implementations and the full cross-language interop matrix are validated against this repo's corpus. TypeScript's reusable runtime now lives in src/taut/gen/runtime/typescript/; other worked interop slices are disposable generated outputs, not durable source.

Docs

  • Overview — the mental model and the shape catalog.
  • Getting Started — author → validate → encode → generate.
  • Reference — every DSL helper, the shapes, the validator rules.
  • Building a Server — handlers, shape engines, the WS loop.
  • examples/tasks — a complete runnable API with generated code for all four languages.

Develop

python run_tests.py        # regenerate the corpus, run the suite

Design notes and the decisions log live in dev-docs/.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

taut_proto-0.7.0.tar.gz (477.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

taut_proto-0.7.0-py3-none-any.whl (134.3 kB view details)

Uploaded Python 3

File details

Details for the file taut_proto-0.7.0.tar.gz.

File metadata

  • Download URL: taut_proto-0.7.0.tar.gz
  • Upload date:
  • Size: 477.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for taut_proto-0.7.0.tar.gz
Algorithm Hash digest
SHA256 0e3f88cb694808c73548aaf941150da92959163833e75413df22d38c5f032296
MD5 b66a8784b27ebe99899c9fdd4c5bc804
BLAKE2b-256 f9ba32f052ef5fcb767a3285603cab61bbfe86474fb25316224e9738bae35c0b

See more details on using hashes here.

Provenance

The following attestation bundles were made for taut_proto-0.7.0.tar.gz:

Publisher: publish.yml on owebeeone/taut

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file taut_proto-0.7.0-py3-none-any.whl.

File metadata

  • Download URL: taut_proto-0.7.0-py3-none-any.whl
  • Upload date:
  • Size: 134.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for taut_proto-0.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4d64acf1a8d53887f7bdeae7990b0307e9373215a8691fa19dccbc3e8cc6029e
MD5 ca993e5ab5422c964fadd46d13671a07
BLAKE2b-256 be76f17cdf9c6e5ed6eedbdf729e46f24964200531fa5d1dc9a4ba1f1d1a11bb

See more details on using hashes here.

Provenance

The following attestation bundles were made for taut_proto-0.7.0-py3-none-any.whl:

Publisher: publish.yml on owebeeone/taut

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page