The client-side Rust crates for Tapes: what a coding-agent harness needs in order to run under capture, what capture puts on the wire, and how a client reads the results back.
Three published crates and one internal test-support crate, in one workspace.
They are consumed by tapesctl and by closed-source clients alike, which is the
point: parity between two clients' start commands is structural, not
policed, because the same code runs in both. A behaviour that lives here
cannot differ between clients; a behaviour that lives in a client can, and that
is the test for whether something belongs in this repository at all.
Two documentation surfaces, and they answer different questions. What these
crates are for, how to add a harness, and the contracts they seal are at
tapes.dev/docs/tapes-crates. The API of
each published crate is on docs.rs:
tapes-harnesses,
tapes-capture,
tapes-client.
All three published crates are supported public API. None of them is a
private helper that happens to be published: tapes-capture is the capture
protocol, tapes-harnesses is the harness knowledge, tapes-client is the read
client. Each is meant to be depended on directly, on its own version, and each
is versioned against the promise below.
Each crate owns one question. The boundaries below are the contract this repository publishes — a change that moves a responsibility across one of these lines is a breaking change even when every signature still compiles.
| crate | owns | does not own |
|---|---|---|
tapes-harnesses (crates/tapes-harnesses/) |
Harness launch and attribution knowledge. The registry, launch recipes, config patch grammars, plugin artifacts, per-harness attribution lanes, transcript discovery and packaging. | Anything true of every harness — that is tapes-capture. |
tapes-capture (crates/tapes-capture/) |
The capture protocol. The X-Tapes-* envelope producer and the harness-id vocabulary it stamps, the capture-gateway environment contract, the launch-nonce protocol, peer-PID lookup, and the peer-trust ancestry walk. |
Any harness's name, and any knowledge that arrives because a harness was added. |
tapes-client (crates/tapes-client/) |
The read surface. The sealed core contract (vendored from a published release asset) and a deployment's discovered cassettes, driven over one transport seam — with the error taxonomy, decode policy, pagination convention, and path join written once beneath both. | Authentication, tenancy, transport, and rendering. Each is a consumer's, and each consumer's answer differs. |
The membership test for the first two is: would adding one more harness change
this? If yes it is tapes-harnesses; if no it is tapes-capture. The
dependency edge runs one way and Cargo enforces it rather than review —
tapes-harnesses depends on tapes-capture, never the reverse. The moment a
capture primitive knows a harness's name it has stopped being the thing every
harness shares. Where capture needs something from a harness — the envelope
needs a session's fields — it declares a trait and the harness crate implements
it.
The test for the third is different, because tapes-client is not split by
subject matter but by when the operation table is known: the core contract is
sealed at build time, a deployment's cassettes are discovered at runtime. Both
halves are thin method tables over one shared floor. That is the whole design —
see crates/tapes-client/README.md.
tapes-client was formed by merging two earlier crates, tapes-read-contract
and tapes-cassette-client. A consumer still pinning either name will find the
item-by-item mapping — including the one variant that was renamed — in that
same README, under Migrating from tapes-read-contract /
tapes-cassette-client.
Nothing specific to one consumer belongs in any of the three: no auth headers, no endpoints, no branding in behaviour. Delivery, auth, and retry live in each consumer.
The repository contains a fourth crate, tapes-mock-upstream. It is internal
test support — a streaming mock provider upstream, a mock ingest server, and the
scripted recipes behind the harness regression matrix — and it is never
released: the release workflow accepts tags for the three published crates only,
and the crate itself sets publish = false. It is a crate rather than a test
module because the matrix launches real harness binaries through
tapes-harnesses rather than inside it, and an integration test in a sibling
crate can only reach items the tested crate exports — so a feature gate would
have made a mock HTTP server part of tapes-harnesses' public surface, and
feature unification would then link it into consumers' production builds.
Consumers who want it for their own tests take it as a git dependency under
[dev-dependencies]; it makes no stability promise. The reasoning is in
crates/tapes-mock-upstream/README.md.
All three crates are live on crates.io — each first published at 0.1.0 —
so cargo add tapes-capture, cargo add tapes-harnesses, and
cargo add tapes-client resolve against the real index, and every PR proves
the crates have stayed publishable.
Each crate is released on its own version and its own <crate>-v<version> tag,
cut from main by the Cut Release dispatch workflow. They version
independently: a fix to the read client is not a reason to renumber the
capture protocol.
Semantics, pre-1.0. While these crates are 0.x, versions carry the usual
Cargo meaning — a breaking change bumps the minor (0.2.0), anything
compatible bumps the patch (0.1.1). The boundary table above is what
"breaking" is measured against, which is why it is written as a contract rather
than as a description of where files currently sit.
Only one dependency edge exists — tapes-harnesses requires tapes-capture —
so tapes-capture publishes first whenever both move, and tapes-client is
free of the ordering entirely.
docs/releasing.md has the order, the tag scheme, the
gates that keep publishability from rotting between releases, and the two-lock
hold on the upload step: the publish runs only while the repository variable
PUBLISH_ENABLED is true, and it is.
Teaching this repository about a new coding agent starts with one const in
crates/tapes-harnesses/src/harness.rs.
docs/adding-a-harness.md walks the whole path: the
registry declaration, when a launch recipe is needed, which attribution strategy
applies, and what the tapes deriver needs on its side.
The X-Tapes-* envelope is a cross-language contract: tapes-capture produces
it in Rust, and the Go parsers in tapes' ingest and gateway capture read it
back. The shared fixture corpus is vendored under
crates/tapes-capture/vendor/tapes-envelope-fixtures/ (authored in the tapes
repository at fixtures/envelope/) and the producer-side oracle in
crates/tapes-capture/src/envelope_fixtures.rs runs against it — the same
corpus the Go parsers test against. scripts/sync-envelope-fixtures.sh
refreshes the copy and detects drift.
The corpus ships a DIGEST sealing its case set, and make corpus-seal
recomputes it over the vendored files. Conformance to a copy only proves parity
if every copy is the same corpus — without the seal, a hand-edit here would
leave the Rust producer and the Go parsers testing against different bytes with
both suites green. The seal needs no network and no tapes checkout, so unlike
the read contract below it has nothing to configure: it either matches or CI is
red.
tapes-client vendors the published tapes read contract at
crates/tapes-client/contracts/tapes-api.yaml, pinned by fingerprint in the
PROVENANCE.md beside it. make contracts-check verifies the vendored bytes
against both that fingerprint and the published release asset, and CI runs it
with TAPES_CONTRACT_STRICT=1 so a gate that cannot reach its input fails
rather than reporting a comparison it never made.
The repository is one workspace whose members are crates/*, so a bare cargo
invocation at the root covers every crate — including crates added after a
command was written. The Nix flake dev shell pins the toolchain via
rust-toolchain.toml:
nix develop
make check # build + fmt-check + clippy + testEvery crate denies unwrap, expect, and panic through the workspace lint
table; return Result and surface errors through the crate error types
instead.
Dual-licensed under either of
- Apache License, Version 2.0 (LICENSE-APACHE)
- MIT license (LICENSE-MIT)
at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.