Adapter process protocol
The desktop launches an adapter and talks to it over standard input and output using length-prefixed JSON. The Rust SDK implements all of this; the details matter when you debug, or when you implement the protocol in another language.
Availability
Protocol version 1. Transport is the adapter process's stdin and stdout, always — never a socket, never a port. Standard error is discarded by the host, so it cannot be used to report anything.
Interface
Framing
Each message is a 4-byte big-endian length followed by exactly that many bytes of UTF-8 JSON. The maximum frame is 4 MiB; a zero-length or oversized frame ends the connection.
Envelopes
Four message types, distinguished by type. Unknown fields are rejected.
| Direction | type | Fields |
|---|---|---|
| Host → adapter | request | v, id, method, params |
| Host → adapter | cancel | v, id |
| Adapter → host | result | v, id, value |
| Adapter → host | error | v, id, code, message |
There is no event or notification type: an adapter never speaks unprompted. Streaming output, such as console data, is polled by the host with a waiting read.
Handshake
The first request is always id: 1, method: "system.adapter.initialize", with:
{ "protocol": 1, "configuration": { "…": "your configuration fields" } }Reply with the services you provide:
{ "protocol": 1, "services": [ { "id": "example.device", "version": 1, "methods": ["example.device.read"] } ] }The catalog is validated before anything else runs: identifiers are dotted and namespaced, may not be or start with system, versions start at 1, method lists are non-empty, every method is prefixed with its service id, and nothing is duplicated. An invalid catalog ends the connection rather than starting a half-working adapter.
Errors
{ "type": "error", "v": 1, "id": 7, "code": "unavailable", "message": "Unknown method" }code must be one of invalid, closed, aborted, denied, unavailable, busy, failed, deadline; anything else becomes failed. Messages are capped at 4096 bytes. The host adds its own judgement of whether an outcome is uncertain when it reports the failure upwards.
Identities, ordering and concurrency
- Request identities increase strictly, up to 2^53−1. Replies may arrive in any order.
- A reply to an unknown or already-completed identity ends the connection.
cancelrefers to an identity already sent; it is advisory.- 32 requests may be in flight; beyond that, reply
busy. - Requests before a successful initialize are refused.
- A method you did not advertise is refused by the host before it reaches you.
Time limits
| Limit | Value |
|---|---|
| Standard call deadline | 30 seconds |
| Console close and transfer abort | 3 seconds |
| Pipe write timeout | 10 seconds |
| Disconnect grace | 5 seconds |
| Process cleanup | 2 seconds |
A deadline that has already expired fails before dispatch, and the host records that the operation never started.
Examples
A complete exchange, from the host's point of view:
→ {"type":"request","v":1,"id":1,"method":"system.adapter.initialize","params":{"protocol":1,"configuration":{}}}
← {"type":"result","v":1,"id":1,"value":{"protocol":1,"services":[{"id":"example.device","version":1,"methods":["example.device.read"]}]}}
→ {"type":"request","v":1,"id":2,"method":"example.device.read","params":{"probe":"inlet"}}
← {"type":"result","v":1,"id":2,"value":{"celsius":21.5}}
→ {"type":"request","v":1,"id":3,"method":"example.device.read","params":{"probe":"slow"}}
→ {"type":"cancel","v":1,"id":3}
← {"type":"error","v":1,"id":3,"code":"aborted","message":"Read canceled"}Cooperative cancellation in Rust:
async fn call(&self, method: &str, params: Value, context: RequestContext) -> Result<Value, CallError> {
tokio::select! {
_ = context.canceled() => Err(CallError::new("aborted", "Canceled")),
result = self.read_device(¶ms) => result,
}
}A cancelled handler keeps its slot until it returns, so return promptly rather than ignoring the signal.
Errors
What the host does when the protocol is broken — each ends that connection, fails everything pending as closed, and marks the outcome uncertain:
| Reported | Cause |
|---|---|
The adapter process exited | The process ended. |
The adapter output ended or violated the protocol | Bad framing, or something printed to stdout. |
The adapter sent an invalid response | An envelope that does not parse. |
The adapter replied to an unknown request | A reply to an unsent or finished identity. |
The adapter input pipe failed or stalled | Writes to the adapter timed out. |
Adapter request identity space exhausted | More than 2^53−1 requests in one connection. |
One violation is survivable: a request larger than the frame limit is refused with invalid — Request exceeds the adapter frame limit; use a paged or streamed service — and the connection continues. Violations are per process: one failing adapter does not disturb another.
Lifecycle and permissions
The host launches one process per connection source, from an absolute path, never through a shell — and on Windows only an explicit .exe, not a batch file or shortcut. On Windows the process runs inside a job object, so it cannot outlive the desktop; equivalent supervision elsewhere is not implemented.
Adapters run with the user's operating-system permissions. Protocol limits are about robustness — frame sizes, concurrency, deadlines — not containment.
Cleanup is bounded and verified: if the host cannot confirm that a process tree ended, it keeps the package's files reserved rather than releasing bytes a running process may still use.
Related guides
- Build your first adapter — the SDK that implements this.
- Adapter services — the methods to advertise.
- Adapter diagnostics — the host's view of a failing connection.
- Filesystem contracts and SDK — the separate protocol for bridge applications.