Harness Protocol¶
Protocol reference for harness clients, NDJSON traffic, and deterministic test events.
Use Deterministic Harness first if you need the mental model before the wire contract.
Keep client tests pinned to this contract.
Protocol Reference¶
trust-harness is the canonical deterministic executor for fast ST automation
outside the full runtime lifecycle.
It is designed for:
- agent repair loops
- CI validation
- docs/examples that need executable behavior
- future website sandboxes and local-model evaluation loops
Transport¶
- process-local only
- newline-delimited JSON over
stdin/stdout - one request per line, one response per line
Each response is either:
{"ok":true,"protocol_version":2,"data":{...}}
or:
{"ok":false,"protocol_version":2,"error":{"kind":"invalid_argument","message":"...","data":null}}
Malformed JSON, an unsupported command, or a failed command produces one error response for that input line. The process then continues with the same session and reads the next non-empty line. Process startup fails only when command-line or protocol-version selection is invalid, or when the transport itself cannot be read or written.
Protocol Versions¶
Protocol version 2 is the default. Set
TRUST_HARNESS_PROTOCOL_VERSION=1|2, --protocol-version 1|2, or
--protocol-version=1|2 to select explicitly. A command-line selection
overrides the environment. Any other argument, a missing command-line value,
or a version other than 1 or 2 fails process startup.
Every line response includes the selected protocol_version, including
errors. Version 2 reports each watched value independently:
{
"values": {
"ready": {"status":"ok","value":{"type":"BOOL","value":true}},
"missing": {
"status":"error",
"code":"unresolved_name",
"message":"boundary path 'missing' did not resolve to a declared value",
"path":"missing",
"candidates":[]
}
}
}
Version 1 preserves the legacy name -> typed value watch shape. Because that
shape cannot represent an entry error, any failed watch promotes the complete
request to a boundary error response.
Commands¶
| Command | Purpose |
|---|---|
load |
Load one or more ST sources into a fresh harness |
reload |
Reload source(s) while preserving retain semantics where supported |
cycle |
Execute one or more cycles |
set_input |
Set a named input/global/program variable |
get_output |
Read a named output/global/program variable |
set_access |
Write a VAR_ACCESS binding |
get_access |
Read a VAR_ACCESS binding |
bind_direct |
Bind a named variable to a direct I/O address |
set_direct_input |
Write to a direct input address |
get_direct_output |
Read from a direct output address |
advance_time |
Advance virtual time without executing a cycle |
run_until |
Cycle until a named output matches an expected value |
restart |
Restart the harness runtime (cold or warm) |
snapshot |
Return watched values without executing more work |
Source Loading¶
Single-source load:
{"cmd":"load","source":"PROGRAM Main\nEND_PROGRAM\n"}
Multi-source load:
{"cmd":"load","sources":["PROGRAM Main\nEND_PROGRAM\n","FUNCTION_BLOCK Fb\nEND_FUNCTION_BLOCK\n"]}
load performs an initial cycle and fails if that first cycle reports runtime
errors.
reload uses the same source / sources parameters.
sources must contain at least one source. When both source and sources
are present, sources is authoritative. An explicitly empty authoritative
sources list is an invalid_argument error; omitting both fields is an
invalid_request error. A successful load replaces the session with a fresh
harness only after compilation and its initial cycle succeed. A failed load
preserves the previous loaded session, or leaves the session unloaded if none
existed.
reload requires a loaded session. It preserves supported retained values,
virtual time, and cycle count. A compilation or retained-state migration
failure preserves the complete previous session.
Cycle Control¶
Advance ten cycles while moving virtual time forward by 10 ms each cycle:
{"cmd":"cycle","count":10,"dt_ms":10,"watch":["q","et"]}
Advance virtual time only:
{"cmd":"advance_time","duration_ms":25}
Take a passive snapshot:
{"cmd":"snapshot","watch":["motor_run","fault","et"]}
cycle defaults to count: 1, dt_ms: 0, and an empty watch list.
advance_time accepts duration_ms; the legacy dt_ms spelling remains an
alias, and duration_ms wins when both are present. snapshot does not run a
cycle or advance virtual time. run_until checks the current value before its
first cycle, so an already-matching value reports cycles_ran: 0. Earlier
completed cycles and time advances remain committed if a later requested cycle
or bounded run fails. Negative time increments return invalid_argument.
I/O Manipulation¶
Set input:
{"cmd":"set_input","name":"start_pb","value":{"type":"BOOL","value":true}}
Get output:
{"cmd":"get_output","name":"motor_run"}
Write/read VAR_ACCESS:
{"cmd":"set_access","name":"RemoteSpeed","value":{"type":"INT","value":42}}
{"cmd":"get_access","name":"RemoteSpeed"}
Bind/read direct I/O:
{"cmd":"bind_direct","name":"start_pb","address":"%IX0.0"}
{"cmd":"set_direct_input","address":"%IX0.0","value":{"type":"BOOL","value":true}}
{"cmd":"get_direct_output","address":"%QX0.0"}
Bounded Run Loop¶
{
"cmd":"run_until",
"name":"q",
"equals":{"type":"BOOL","value":true},
"dt_ms":10,
"max_cycles":5,
"watch":["q","et"]
}
If max_cycles is exceeded, the protocol returns:
{
"ok": false,
"error": {
"kind": "run_until_timeout",
"message": "run_until exceeded 5 cycles before 'q' matched the expected value",
"data": {
"name": "q",
"max_cycles": 5,
"expected": {"type":"BOOL","value":true}
}
}
}
Restart¶
{"cmd":"restart","mode":"cold"}
{"cmd":"restart","mode":"warm"}
The mode defaults to cold and is ASCII case-insensitive. warm preserves
variables declared RETAIN; cold restores their declared initial values.
Unsupported modes return invalid_request.
Typed Value Format¶
The protocol uses a stable typed JSON shape.
Common scalar examples:
{"type":"BOOL","value":true}
{"type":"INT","value":7}
{"type":"DINT","value":42}
{"type":"REAL","value":1.5}
{"type":"TIME","nanos":30000000}
{"type":"STRING","value":"hello"}
Structured examples:
{"type":"ARRAY","dimensions":[[0,1]],"elements":[{"type":"BOOL","value":true},{"type":"BOOL","value":false}]}
{"type":"STRUCT","type_name":"MyStruct","fields":{"enabled":{"type":"BOOL","value":true}}}
{"type":"ENUM","type_name":"Mode","variant":"Auto","numeric":1}
{"type":"NULL"}
Error Kinds¶
Current stable error.kind values:
invalid_requestinvalid_argumentnot_loadedcompile_errorruntime_errorruntime_cycle_errorrun_until_timeout
Boundary failures use their stable boundary code as error.kind:
unresolved_nameunbound_programambiguous_nameunsupported_path_syntaxwrong_kindundeclared_bindinginternal_lock_failureinternal_failure
Boundary error data contains path when one exists and a deterministic
candidates array. runtime_cycle_error data contains the ordered runtime
error strings. run_until_timeout data contains the target name, cycle
budget, and expected typed value. Missing request fields, unsupported commands,
and unsupported restart modes are structural invalid_request errors;
semantic value and non-negative-duration validation uses invalid_argument.
Current Observability Scope¶
The protocol currently returns watched value snapshots. Full trace streaming is still deferred, but the watch output is already enough for:
- timer/state assertions
- small repair loops
- deterministic evidence capture in docs and CI
That keeps the surface small while still making the harness useful today.