Skip to content

JSON-RPC Protocol

The Attest engine communicates with SDKs via JSON-RPC 2.0 over NDJSON (newline-delimited JSON) on stdin/stdout. Each message is a single JSON object terminated by \n.

PropertyValue
ProtocolJSON-RPC 2.0
EncodingNDJSON (one JSON object per line)
Transportstdin (SDK to engine), stdout (engine to SDK)
Engine stderrLog output (not protocol traffic)
EncodingUTF-8, compact JSON (no whitespace)

The SDK writes requests to the engine’s stdin and reads responses from stdout. The engine’s stderr carries log messages and is not part of the protocol.

{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": { ... }
}
FieldTypeDescription
jsonrpcstringAlways "2.0"
idintegerAuto-incrementing request ID for response correlation
methodstringRPC method name
paramsobjectMethod-specific parameters
{
"jsonrpc": "2.0",
"id": 1,
"result": { ... }
}
{
"jsonrpc": "2.0",
"id": 1,
"error": {
"code": 1001,
"message": "Invalid trace: missing required field 'output'",
"data": {
"error_type": "validation",
"retryable": false,
"detail": "Trace output is required"
}
}
}
FieldTypeDescription
error.codeintegerError code (see table below)
error.messagestringHuman-readable error description
error.dataobject | nullStructured error metadata
error.data.error_typestringError category
error.data.retryablebooleanWhether the request can be retried
error.data.detailstringAdditional context

Handshake between SDK and engine. Must be the first request.

Request:

{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"sdk_name": "attest-python",
"sdk_version": "0.4.2",
"protocol_version": 1,
"required_capabilities": ["layers_1_4"],
"preferred_encoding": "json"
}
}
ParamTypeDescription
sdk_namestringSDK identifier (attest-python or attest-typescript)
sdk_versionstringSDK version
protocol_versionintegerRequested protocol version (currently 1)
required_capabilitiesstring[]Capabilities the SDK requires
preferred_encodingstringWire encoding preference ("json")

Response:

{
"jsonrpc": "2.0",
"id": 1,
"result": {
"engine_version": "0.4.0",
"protocol_version": 1,
"capabilities": ["layers_1_4", "layers_5_6", "layer_7", "layer_8"],
"missing": [],
"compatible": true,
"encoding": "json",
"max_concurrent_requests": 64,
"max_trace_size_bytes": 10485760,
"max_steps_per_trace": 10000
}
}
FieldTypeDescription
engine_versionstringEngine binary version
protocol_versionintegerNegotiated protocol version
capabilitiesstring[]Available engine capabilities
missingstring[]Requested capabilities not available
compatiblebooleantrue if all required capabilities are present
encodingstringNegotiated encoding
max_concurrent_requestsintegerMax parallel evaluations
max_trace_size_bytesintegerMax trace payload size in bytes
max_steps_per_traceintegerMax steps allowed per trace

Evaluate a batch of assertions against a trace.

Request:

{
"jsonrpc": "2.0",
"id": 2,
"method": "evaluate_batch",
"params": {
"trace": {
"schema_version": 1,
"trace_id": "trc_abc123def456",
"agent_id": "weather-agent",
"input": {
"query": "weather in tokyo"
},
"steps": [
{
"type": "llm_call",
"name": "gpt-4.1",
"args": { "messages": [{"role": "user", "content": "weather in tokyo"}] },
"result": { "content": "Let me check." }
},
{
"type": "tool_call",
"name": "get_weather",
"args": { "city": "tokyo" },
"result": { "temp_c": 22, "condition": "sunny" }
}
],
"output": {
"message": "Tokyo is 22C and sunny."
},
"metadata": {
"total_tokens": 350,
"cost_usd": 0.001,
"latency_ms": 800
},
"parent_trace_id": null
},
"assertions": [
{
"assertion_id": "assert_a1b2c3d4",
"type": "content",
"spec": {
"target": "output.message",
"check": "contains",
"value": "tokyo",
"case_sensitive": false
}
},
{
"assertion_id": "assert_e5f6g7h8",
"type": "constraint",
"spec": {
"field": "metadata.cost_usd",
"operator": "lte",
"value": 0.01
}
}
]
}
}

Response:

{
"jsonrpc": "2.0",
"id": 2,
"result": {
"results": [
{
"assertion_id": "assert_a1b2c3d4",
"status": "pass",
"score": 1.0,
"explanation": "output.message contains 'tokyo' (case-insensitive)",
"cost": 0.0,
"duration_ms": 0
},
{
"assertion_id": "assert_e5f6g7h8",
"status": "pass",
"score": 1.0,
"explanation": "metadata.cost_usd (0.001) <= 0.01",
"cost": 0.0,
"duration_ms": 0
}
],
"total_cost": 0.0,
"total_duration_ms": 1
}
}
FieldTypeDescription
assertion_idstringMatches the request assertion ID
statusstring"pass", "soft_fail", or "hard_fail"
scorefloat0.0 to 1.0 confidence score
explanationstringHuman-readable reason
costfloatUSD cost of this assertion (0 for layers 1-4)
duration_msintegerEvaluation time
request_idstring | nullOptional correlation ID

Submit an externally-computed plugin result to the engine.

Request:

{
"jsonrpc": "2.0",
"id": 3,
"method": "submit_plugin_result",
"params": {
"trace_id": "trc_abc123def456",
"plugin_name": "toxicity-checker",
"assertion_id": "assert_plugin_01",
"result": {
"assertion_id": "assert_plugin_01",
"status": "pass",
"score": 0.95,
"explanation": "No toxic content detected",
"cost": 0.0,
"duration_ms": 50
}
}
}

Response:

{
"jsonrpc": "2.0",
"id": 3,
"result": {
"accepted": true
}
}

Gracefully terminate the engine. Returns session statistics.

Request:

{
"jsonrpc": "2.0",
"id": 4,
"method": "shutdown",
"params": {}
}

Response:

{
"jsonrpc": "2.0",
"id": 4,
"result": {
"sessions_completed": 1,
"assertions_evaluated": 47
}
}
CodeConstantDescription
1001ERR_INVALID_TRACETrace validation failed (missing fields, invalid schema)
1002ERR_ASSERTION_ERRORAssertion evaluation error (invalid spec, unknown type)
2001ERR_PROVIDER_ERRORExternal provider error (LLM API failure, embedding error)
3001ERR_ENGINE_ERRORInternal engine error
3002ERR_TIMEOUTRequest timed out
3003ERR_SESSION_ERRORSession state error (not initialized, already shutdown)

The type field in assertion objects maps to pipeline layers:

TypeLayerCostDescription
schema1FreeJSON Schema validation
constraint2FreeNumeric/string constraints
trace3FreeStep ordering and tool verification
content4FreeString matching, regex, keywords
embedding5~$0.001Semantic similarity via embeddings
llm_judge6~$0.01Natural language evaluation
trace_tree7FreeMulti-agent delegation analysis

When ATTEST_SIMULATION=1 is set, the SDK bypasses the engine entirely. evaluate_batch returns deterministic pass results for all assertions with zero cost. No subprocess is spawned.

{
"results": [
{
"assertion_id": "assert_a1b2c3d4",
"status": "pass",
"score": 1.0,
"explanation": "[simulation] content assertion passed (deterministic)",
"cost": 0.0,
"duration_ms": 0
}
],
"total_cost": 0.0,
"total_duration_ms": 0
}

The AttestClient wraps EngineManager with request/response correlation:

  • A background reader loop reads NDJSON lines from stdout and resolves pending futures by request ID
  • A write lock serializes outbound requests (stdin is sequential)
  • Multiple callers can await independent futures concurrently
  • The pytest plugin runs the engine event loop on a dedicated daemon thread, making both sync (evaluate) and async (evaluate_async) paths safe from any calling context