SDKs
SDKs
The tool endpoint is a signed POST and a JSON body, so you can write one by hand in any language — the contract pages exist so you can. These libraries exist because four parts of it are easy to get subtly, silently wrong.
What they do
| Node | sdk/node — plus the tether CLI |
| Python | sdk/python — 3.8+, standard library only |
| .NET | sdk/dotnet — net8.0, no package references |
Each one owns the same four refusals, with the same error codes:
| What arrives | What happens |
|---|---|
| A body that does not match its signature | 401 invalid_signature, compared in constant time |
| A timestamp outside the 300s tolerance | 401 stale_timestamp — a captured request cannot be replayed later |
| A write that did not arrive in execute mode | 400 execute_mode_required, and your function does not run |
A request_id you have already answered | the first result, X-Tether-Replayed: true, your function not re-run |
Plus one thing that is not a refusal: build_catalog derives the catalog you register from the
same definitions the endpoint dispatches, so the schema Tether holds cannot drift from the
function that serves it.
They do not decide what a write is
Every SDK reads the write flag from an explicit declaration and from nothing else. Not the tool name, not the HTTP verb, not a heuristic.
// Node. This is a read. It will be dispatched inline, without a confirmation.
delete_everything: {
description: 'Reads a report and changes nothing at all.',
handler: () => report(),
}
That looks alarming and it is the point. delete_everything is a read until you say otherwise,
because the alternative — a library guessing from a name — is a library that guesses wrong on
someone's archive_customer and executes it inline. You declare it; nothing infers it.
Why they are not the same shape
| SDK | Entry point |
|---|---|
| Node | createHandler({ secret, tools }) returns an express (req, res) |
| Python | Tether(...).handle(headers, raw_body) returns a Response |
| .NET | new TetherEndpoint(...).HandleAsync(headers, rawBody) returns a ToolResponse |
Node returns a handler because express is what a Node service runs, and returning anything else would mean a customer writing glue for a framework the SDK could have targeted.
Python and .NET return the answer rather than serving it, because neither ecosystem has a default worth binding the contract to. FastAPI, Flask, Django, a minimal API and a controller are all three lines from a function that takes headers and a body — and none of them would have been three lines from a handler shaped for one of the others.
Idempotency is per-process by default
Each SDK ships an in-memory store for replayed requests. It forgets on restart and knows nothing about your other instances, so behind a load balancer a replay reaches a process that never saw the original — and runs your function a second time.
For a read that is a wasted query. For a write it is a duplicate write. Every SDK takes a store you supply; back it with Redis or your database before a write tool goes live.
One contract, four implementations
The engine, and all three SDKs, read the same file: header names, worked signature vectors, and the exact validation messages.
Before it existed, each implementation was tested against itself. Renaming a header in the engine would leave every test suite green and break every endpoint built on every SDK, and the only thing standing between that and production was somebody remembering to re-read four files at once.
Now a change in one fails in the other three. The vectors deliberately include an empty body and non-ASCII in both the body and the secret, because UTF-8 handling is where independent implementations actually drift apart.
Which to use
Whichever language your service is already written in. There is no capability difference between them: the same four refusals, the same codes, the same catalog output.
The tether CLI lives in the Node package and stays there — lint, register, try, import
and actions all speak the same HTTP API, so it works against a Python or .NET endpoint exactly
as well. A second CLI would be a second place for the contract to drift.
If your language is not here, the tool endpoint contract is six pages and a worked example. It is genuinely writable by hand — read Verifying signatures first, and Retries, timeouts, idempotency second.
Related
- The tool endpoint — the contract the SDKs implement
- Request envelope — exactly what arrives, field by field
- Tool spec fields — every field a catalog entry can carry
- The tether CLI — register and debug from any language