tapes-harnesses
tapes-harnesses is the half of capture that changes because a harness was
added. Its membership test is one question: would adding one more harness
change this? If yes it belongs here; if no it belongs in
tapes-capture, which this crate depends on and which may
never depend back.
One naming note before anything else: tapes-harnesses is a crate in the
tapes-crates repository, not a repository of its own — see
the names.
cargo add tapes-harnessesFull API documentation is on docs.rs. The crate has no feature flags: everything below is always compiled.
The registry
Section titled “The registry”harness::REGISTRY holds one declaration per harness, bundling its id,
aliases, User-Agent rule, launch support, attribution strategy, transcript
location, and plugin needs. Everything else derives: find resolves a
user-typed name through the aliases, for_user_agent resolves a request’s
User-Agent, and supported_agents is the launchable subset. Consumers
derive their supported-agent lists from the registry rather than hard-coding
one, so a new entry appears in their surface without their doing anything —
adding a harness is additive.
This compiles and runs against the published crate:
use tapes_harnesses::harness;
fn main() { // Resolve a user-typed spelling through ids and aliases, case-insensitively. if let Some(h) = harness::find("Claude-Code") { println!("{}: launch={:?} attribution={:?}", h.id(), h.launch(), h.attribution()); }
// The launchable subset, derived from the registry rather than restated. println!("launchable: {}", harness::supported_agents().join(", "));}Output:
claude: launch=Recipe attribution=SessionsDirlaunchable: claude, codex, opencode, piThe five axes
Section titled “The five axes”A Harness declaration is a classification along five orthogonal enums, and
they are the vocabulary the rest of the crate speaks:
UserAgentMatch— how a request’sUser-Agentidentifies the harness:None, or a case-insensitivePrefix.LaunchSupport— whether this crate can plan a launch:Recipe(the crate ships one),ConsumerOwned(launchable, but the consumer plans the argv itself), orUnsupported.AttributionStrategy— how a capture client recovers a session’s identity:SessionsDir,OpenRollout,SelfAttributing,LifecycleHooks, orNone.TranscriptSource— where the harness’s on-disk transcripts live:ClaudeProjects,CodexRollouts, orNone.PluginDelivery— whether capture needs an artifact installed into the harness:None,BundledExtension(the crate ships the files), orHookManifestTemplates(the crate ships manifest templates with consumer-rendered slots).
All five are #[non_exhaustive] on purpose: a new harness can add a variant
without a breaking change.
The registry today
Section titled “The registry today”| harness | User-Agent | launch | attribution | transcripts | plugin |
|---|---|---|---|---|---|
claude |
prefix claude |
Recipe |
SessionsDir |
ClaudeProjects |
None |
codex |
none | Recipe |
OpenRollout |
CodexRollouts |
None |
codex-app |
none | Unsupported |
LifecycleHooks |
CodexRollouts |
HookManifestTemplates |
opencode |
none | Recipe |
SelfAttributing |
None |
BundledExtension |
pi |
none | ConsumerOwned |
SelfAttributing |
None |
BundledExtension |
Only Claude is identified by User-Agent; the others are recognised by route,
launch marker, lifecycle report, or their own envelope. Note that codex-app
is a distinct harness, not an alias of codex: it shares Codex’s wire
protocol and rollout tree, but it is a long-lived host a consumer configures
rather than launches, and its identity arrives through lifecycle hook reports.
The three ways a harness gets captured
Section titled “The three ways a harness gets captured”This is the distinction to hold on to, because it decides which modules apply
to a given harness. It correlates with AttributionStrategy but is not the
same axis: that enum says how a request acquires an identity, this says how
the traffic is reached at all.
| mechanism | when it applies | plan it with |
|---|---|---|
| Launch redirect — point the harness’s base-URL knob at a proxy | the harness has such a knob (claude, codex, opencode) |
launch |
| Installed plugin — code runs inside the harness and stamps its own envelope | the harness has no such knob (pi) |
plugin |
| Lifecycle hooks — a hook plugin reports allowlisted evidence at session boundaries | the harness is configured rather than launched (codex-app) |
plugin::codex_app and config |
opencode deliberately has both of the first two: its provider endpoints live in a config document a recipe can plan, but it publishes no session file a client could attribute from, so the bundled extension both redirects and stamps the envelope from inside. The registry declares the combination because the two compose.
Launch recipes
Section titled “Launch recipes”Recipes are pure planners: they plan an argv prefix, an environment overlay, and any config documents a harness reads from disk. Spawning, materialisation, and cleanup stay with the consumer. The registry records that a harness has a recipe; the consumer constructs it with the arguments only the consumer has — Claude needs one endpoint, codex needs an endpoint plus an auth mode and a provider identity, opencode needs an endpoint per provider plus a model.
Plugins, and the gateway environment a launcher sets
Section titled “Plugins, and the gateway environment a launcher sets”For harnesses captured from inside, the crate ships the artifacts themselves —
plugin install in a consumer is a file copy over crate-owned bytes, so no
consumer carries its own drifting fork of the asset. Bundled artifacts must be
vendor-neutral: an asset that names a vendor stays in that consumer’s
repository and gets no registry variant.
An installed artifact reads its instructions from the environment at launch
time. Seven variables make up that contract; the first four are protocol and
live in tapes-capture, the last three are presentation and live here:
| variable | set by | meaning |
|---|---|---|
TAPES_GATEWAY_URL |
launcher | Where to send the harness’s LLM traffic. Unset means “not captured” — the artifact leaves the harness’s own endpoints alone. |
TAPES_GATEWAY_SCHEMA |
launcher | Which upstream schema the proxy fronts (anthropic, openai). A display and diagnostic hint; an artifact must not gate the redirect on it. |
TAPES_GATEWAY_NONCE |
launcher | The per-launch secret. Read once at load and deleted from the process environment immediately, so the harness’s own subprocesses never receive it. |
TAPES_GATEWAY_PROVIDER_ROUTES |
launcher | Set to 1 when the proxy serves each provider on its own route. Unset is the single-upstream shape, which is what a launcher predating this variable gets. |
TAPES_GATEWAY_LABEL |
launcher | The product word shown in a harness’s status entry. |
TAPES_GATEWAY_LABEL_SUFFIX |
launcher | Appended to the status label after the active schema. |
TAPES_GATEWAY_REMEDY |
launcher | The sentence appended to a schema-mismatch warning — the diagnosis is the asset’s, the remedy is the launcher’s, because only the launcher knows which command switches its proxy. |
Attribution
Section titled “Attribution”attribution::attribute (and attribute_with_evidence) is the pipeline: one
request’s facts in, one attribution outcome out, driving the per-harness
primitives in the order that was validated against real traffic. This is
the call a capture client makes. The primitives beneath — session-file
reads, fork-parent recovery, the rollout watcher — are exposed for tests and
unusual clients, grouped per harness (attribution/claude/,
attribution/codex/, attribution/codex_app/) with the harness-agnostic
pieces shared.
The per-harness lanes match the AttributionStrategy axis:
SessionsDir(claude): the harness publishes a PID-indexed session file, so the peer PID of the accepted connection indexes straight to an identity.OpenRollout(codex): nothing is published by PID; identity is recovered from the rollout file a live process holds open, filtered by recency and by the provider the launch configured.SelfAttributing(pi, opencode): the harness stamps its own completeX-Tapes-*envelope from inside. The client’s job is to preserve what arrives — overwriting a complete inbound envelope withunknownwould silently re-file those sessions — and to demand the nonce echo before believing it.LifecycleHooks(codex-app): identity arrives as hook reports at session, prompt, stop, and subagent boundaries. There is no launched PID to anchor peer trust on, so the evidence is allowlisted instead.
Transcripts
Section titled “Transcripts”Wire capture yields a complete call inventory but no causal/fork skeleton —
that lives only in the harness’s on-disk transcript trees, which the
transcript lane uploads. transcript::sweep discovers sessions under a
transcript root (including a startup sweep that recovers sessions which began
and ended while a client was down), trigger is the push policy, and
payload is the ingest payload shape. Delivery, auth, and retry are the
consumer’s.
Config patch grammars
Section titled “Config patch grammars”config owns format-preserving patch grammars for harness config files — how
an installer patches a capture provider into a harness’s own config,
idempotently and without disturbing the user’s content. Where launch plans
per-process config that dies with the process, config owns the durable
install a desktop app or long-lived integration needs.
Adding a harness
Section titled “Adding a harness”Teaching this crate about a new coding agent starts with one const in
harness.rs; invariant tests tell you what else the declaration implies.
Adding a harness walks the whole path, and
the harness regression matrix is the CI floor every
entry has to stand on.