A Rust SDK for terva external processes: the protocol crates and author-side runtimes for terva connectors (connproto) and extensions (extproto), so a Rust connector or extension starts from a conformant wire instead of hand-rolling one.
Extracted from the two Rust implementations that proved the patterns — terva-conn-matrix (the Matrix connector) and terva-ext-index (the tree-sitter outline extension) — and grown by demand, not speculation.
Status: P1-P9 landed — the wire, env, and connproto/connsdk crates
are extracted with their conformance suites (terva's published golden
corpus and the byte-exact host-conversation replay), terva-conn-matrix
runs on them (verified by its hermetic suite and eight-scenario live
harness), terva-extproto covers the full protocol 1..6 vocabulary on the
same published-corpus model, terva-extsdk serves the extension MVP (tools,
standing context, cwd-following, jail) with a replay-style run-loop
suite, terva-ext-index runs on it, the protocol-5 connector
carrier composes the two halves (Extension::connector hosts the
standalone connsdk engine over the chat envelope tunnel, proven
end-to-end by a duplex live-conversation suite), secrets are sealed
at rest (connsdk's SealedState for a standalone connector's own
config, the protocol-6 brokered secret verbs for extensions — verified
against the Go SDK in both directions), and the release model below is
in effect: consumers pin by tag, and the
connector and bridge
templates seed new projects.
crates/
terva-wire — LF-JSON framing: 4 MiB cap, CRLF tolerance, bounded
oversize drain; byte-parity with terva's lineframe
terva-env — terva home/state-dir resolution, 0600/0700 atomic
persistence, the stderr panic hook
terva-connproto — connproto v1..2 frame types, golden-tested against
the host corpus (byte-exact encode / tolerant
decode), plus the additive hello `secrets`
declaration
terva-connsdk — the connector serve loop: Service trait, Responder
(exactly one result per command, enforced by Drop),
FrameSink single-writer discipline, strict handshake,
malformed-command envelope recovery — and
SealedState: per-value at-rest sealing of the
connector's own config, dual-recipient, each value
bound to its path (`load`/`save` for bytes,
`load_as`/`save_as` for a config type)
terva-extproto — extproto frame types (protocol 1..6), golden-tested;
the chat-tunnel envelope carries connproto frames
verbatim (RawValue — provably unperturbed bytes)
terva-extsdk — the extension runtime: Extension builder (tools,
standing context, cwd-following, min-protocol),
Host dirs, the self-Jail discipline, ToolResult,
panic-isolated dispatch, oversize degradation,
the protocol-6 brokered secret verbs (an extension
stores through the host, not a key of its own) —
and, behind the off-by-default `carrier` feature,
the protocol-5 connector carrier: one process that
is also a chat connector, hosting the connsdk
engine over the `chat` envelope tunnel
templates/
bridge — a ready-to-copy extension+connector project (one
tool, standing context, a stub connector service)
connector — a ready-to-copy STANDALONE connector: the four
verbs, and credentials sealed at rest and declared
in the handshake
One protocol family, one wire, composed: Extension::connector hosts
the SAME terva_connsdk::Service a standalone connector implements, so
a connector moves between the two packagings by swapping a few lines of
main. Planned: terva-ctrlproto (later).
Each crate states its contract in its own docs; the summary:
| crate | speaks | oracle |
|---|---|---|
| terva-wire | protocol-agnostic framing (4 MiB cap) | packages/lineframe |
| terva-env | host env conventions, no wire protocol | envcompat / privfs |
| terva-connproto | connproto v1..2, plus the additive hello secrets declaration |
terva's published golden.jsonl, vendored and drift-checked |
| terva-connsdk | serves connproto v1..2 (range via ServeConfig); SealedState seals the connector's own state (enc:age:v2, dual-recipient), bytes via load/save or a config type via load_as/save_as |
the host-conversation replay + a Go-sealed interop fixture (both directions verified) |
| terva-extproto | extension protocol 1..6 vocabulary | terva's published golden.jsonl (89 cases, all 49 types), vendored and drift-checked |
| terva-extsdk | extension protocol ≥ 1 (declare a floor with min_protocol; cwd-following needs ≥ 2; the connector carrier needs the carrier feature and a host ≥ 5; the brokered secret verbs need ≥ 6 and fail fast below it) |
replay + duplex carrier and secrets suites, run both ways round carrier |
terva-extsdk has one: carrier, off by default, enabling the
protocol-5 connector role (Extension::connector). It links the whole
connector SDK, and connsdk's at-rest sealing links age behind it — a
129-package dependency graph against 14 without. A tools-only
extension should not pay for a role it cannot use, so the cost is opt-in.
terva-extsdk = { git = "https://github.com/terva-sh/terva-sdk-rust", tag = "v0.3.4", features = ["carrier"] }When terva bumps a protocol, the matching proto crate re-mirrors before any consumer declares the new version.
- docs/design.md — what the SDK does and how the parts fit, with diagrams and almost no code. Start here to decide whether to use it.
- docs/architecture.md — how each crate is built, which invariants hold it together, and where to add code. For building on it, extending it, or fixing it.
- CHANGELOG.md — what changed between two tags, and what it means for code already written against the older one.
Pin for release, path for development. Releases are annotated
vX.Y.Z tags; all crates version in lockstep. Pin the tag and commit
your Cargo.lock so --locked builds are reproducible:
[dependencies]
terva-extsdk = { git = "https://github.com/terva-sh/terva-sdk-rust", tag = "v0.3.4" }What changed between two tags — and what it means for code already written against the older one — is in CHANGELOG.md.
For an SSH remote, cargo needs the git CLI for fetching — put this in
your project's .cargo/config.toml:
[net]
git-fetch-with-cli = trueTo co-develop against a local checkout without touching the pin, add a
temporary [patch] (drop it — and the Cargo.lock churn — before
committing):
[patch."https://github.com/terva-sh/terva-sdk-rust"]
terva-extsdk = { path = "/path/to/terva-sdk-rust/crates/terva-extsdk" }New projects: copy a template — templates/connector
for a standalone chat connector (four verbs, sealed credentials), or
templates/bridge for an extension, with or without
the connector role. Both are built and linted by CI against the crates
they demonstrate, so neither can rot.
Needs Rust (stable) and just.
just test # cargo test --workspace (+ the template build)
just ci # fmt --check + clippy -D warnings + locked tests + templateDay-to-day history lives on an internal main; the phase plan
(PLAN.md) and release plumbing are maintainer-only and are not part
of the public tree. Only curated release branches reach this mirror.
terva's golden corpora are the oracles. Frames this SDK produces are asserted byte-exact against them; frames the host produces are asserted on decode.
connproto reads terva's published corpus. golden.jsonl is vendored
into crates/terva-connproto/tests/testdata/ and driven case by case —
not transcribed into Rust literals, which is a copy that drifts in
silence. A completeness test requires every case to be exercised in
exactly one direction, so a frame type added upstream fails the suite
instead of sitting unnoticed. Byte-equality with Go's HTML-escaping
encoder is now guaranteed rather than lucky: terva guards its corpus
against acquiring a <, > or &, and this side re-checks it.
extproto reads terva's published corpus too, on the same model, with
one addition: the extension fixture reports a direction per frame
(ext_to_host / host_to_ext / both — the chat tunnel envelope is
emitted by each side), so the classification is read from the fixture
rather than guessed here. Its 89 cases cover all 49 frame types, against
the 41-case sample the old hand-mirror was copied from — the protocol-6
secret verbs and the entire widget vocabulary had no coverage at all.
That new coverage immediately earned itself: it caught this SDK emitting
"value":3.0 where terva emits "value":3 for a whole-numbered meter
widget.
At-rest sealing has an oracle of its own, since a format read from the
source is not the same as one that interoperates: a tree sealed by the
real Go connsdk.SealedState is committed as a fixture this SDK opens,
and the reverse direction — Go's Load opening a tree this SDK sealed
— was verified against the Go SDK directly.