Runtime Engine¶
Runtime-engine lookup for clocks, tasks, retain, launching, process image handling, online change plumbing, and deployment mechanics.
Related: Runtime Semantics, Bytecode Format
Status: Implemented architecture. Production runtime executes STBC bytecode through the VM only; helper evaluation remains for bounded const/debug/config flows and the old evaluator internals are test-only.
1. Purpose¶
This document specifies the architecture for a portable Structured Text (ST) runtime capable of executing IEC 61131-3 compliant programs. The initial implementation targets desktop operating systems (Linux, Windows, macOS); embedded support is planned.
2. Design Goals¶
| Goal | Description |
|---|---|
| Portability | Single runtime codebase runs on desktop and embedded targets |
| Determinism | Predictable scan cycle execution suitable for automation |
| IEC Compliance | Align task scheduling and execution semantics with IEC 61131-3 Ed.3 |
| Simplicity | Minimal clock abstraction surface |
| Testability | Full runtime testable on desktop without hardware |
3. Architecture Overview¶
┌──────────────────────────────────────────────────┐
│ ST Program (Bytecode) │
└──────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ ST Runtime Core │
│ ┌────────────┐ ┌──────────────────┐ ┌──────────────┐ │
│ │ Executor │ │Resource Scheduler│ │ Timer System │ │
│ └────────────┘ └──────────────────┘ └──────────────┘ │
│ ┌────────────────────────────────────────────┐ │
│ │ Process Image │ │
│ └────────────────────────────────────────────┘ │
└──────────────────────┬───────────────────────────┘
▼
┌──────────────────────────────────────────────────┐
│ Clock Trait │
└───────────┬──────────────────────┬───────────────┘
▼ ▼
┌───────────────────┐ ┌───────────────────┐
│ StdClock │ │ ManualClock │
│ (Linux/Win/Mac) │ │ (Tests/Sim) │
└───────────────────┘ └───────────────────┘
4. Clock Abstraction Layer¶
4.1 Rationale¶
The runtime requires only a monotonic clock and a way to sleep until a deadline. Rather than abstracting entire operating systems, we abstract only what the scheduler actually uses. This keeps the abstraction minimal and each clock implementation small.
4.2 Clock Trait Definition¶
pub trait Clock: Send + Sync + 'static {
/// Returns monotonic time for scheduling (nanosecond Duration).
fn now(&self) -> Duration;
/// Sleeps until a target time. Used only by real resource threads.
fn sleep_until(&self, deadline: Duration);
/// Wake any sleepers (best-effort).
fn wake(&self) { /* optional */ }
}
The runtime scheduler uses a Clock for time and pacing. Thread creation and mutexing are handled by Rust’s standard library, keeping the abstraction surface minimal.
4.3 Why Only a Clock¶
| Operation | Justification |
|---|---|
now |
Required for scan cycle timing and IEC timers (TON, TOF, TP) |
sleep_until |
Paces resource cycles in real threads |
wake |
Allows clean shutdown of resource threads |
Notably absent: file I/O (bytecode loaded at init), networking (handled separately via I/O abstraction), explicit mutex APIs (runtime uses RwLock/Mutex internally), dynamic allocation in hot path.
5. Clock Implementations¶
5.1 StdClock (Desktop)¶
Targets: Linux, Windows, macOS
Implementation: Uses Rust standard library (Instant, thread::sleep).
pub struct StdClock {
start: Instant,
}
impl Clock for StdClock {
fn now(&self) -> Duration {
let elapsed = self.start.elapsed();
let nanos = i64::try_from(elapsed.as_nanos()).unwrap_or(i64::MAX);
Duration::from_nanos(nanos)
}
fn sleep_until(&self, deadline: Duration) {
let now = self.now();
let delta = deadline.as_nanos() - now.as_nanos();
if delta <= 0 {
return;
}
let delta = u64::try_from(delta).unwrap_or(u64::MAX);
thread::sleep(std::time::Duration::from_nanos(delta));
}
}
Justification: Rust’s standard library already abstracts Linux/Windows/macOS differences. Task priority is enforced by the runtime scheduler; OS thread priority is best-effort only and may be ignored.
5.2 ManualClock (Tests)¶
Deterministic clock for unit tests and simulation. Time advances explicitly; no real sleeping occurs. Used by scheduler tests and trace reproducibility checks.
Advancement uses saturating signed-nanosecond arithmetic: a delta beyond the
representable Duration range clamps to the corresponding bound and never
wraps or panics. Explicit set_current_time remains the separate mechanism for
placing a test clock at an earlier instant.
5.2.1 Runtime logical time and non-pacing tick¶
StdClock::now exposes nondecreasing elapsed monotonic time. No strict progress
or minimum resolution is guaranteed between adjacent samples.
ResourceRunner::tick is one non-pacing scheduler step. It samples the
injected clock and executes one resource cycle without calling
Clock::sleep_until; pacing and deadline sleeps belong to the spawned resource
loop. A ManualClock therefore remains fixed until explicitly advanced.
The zero-argument truST compatibility call TIME() returns the runtime's
current injected logical elapsed time as a TIME value. It does not read civil
wall time. Harness and simulation advances affect the result only through the
runtime or manual clock.
Periodic task readiness likewise uses only the injected clock. A direct tick with a fixed manual clock does not make the task ready; advancing that clock to the task deadline makes one activation eligible under the readiness contract below.
CURRENT_DT() is separate. It samples the host SystemTime once per call and
returns the UTC Unix timestamp as timezone-naive DT ticks with epoch
1970-01-01T00:00:00Z and fixed one-millisecond resolution. Positive
sub-millisecond fractions are truncated. Tick 0 through i64::MAX are
accepted; a pre-epoch or larger host value returns RuntimeError::Overflow.
Local timezone and DST are not applied.
The injected runtime/manual clock, scheduler scaling, simulation time, and
replay time do not control CURRENT_DT, and its samples are not made
monotonic. A host-clock rollback may therefore produce a lower later value.
Deterministic replay excludes programs that read CURRENT_DT unless the
environment controls the host clock. The complete callable contract is in
docs/specs/07-standard-functions.md#host-civil-clock-current_dt.
5.3 Embedded Clock (Planned)¶
An RTOS-backed clock (e.g., FreeRTOS) is planned for embedded targets. The runtime core remains unchanged; only the Clock implementation differs.
6. Runtime Components¶
6.1 Executor¶
Interprets compiled ST bytecode. Operates on the process image. Pure computation with no platform dependencies.
Design decisions: - Stack-based bytecode VM (simpler than register-based) - No heap allocation during execution (predictable timing) - All state in process image (inspectable, serializable)
6.2 Task Manager (Resource Scheduler)¶
Implements IEC 61131-3 task scheduling and program organization unit (POU) associations (IEC 61131-3 Ed.3, §6.8.2; Tables 62–63).
Each IEC resource runs inside a dedicated scheduler loop. The scheduler is executed on an OS thread started via std::thread::spawn; IEC tasks are not OS threads.
IEC task model: - Tasks are periodic (INTERVAL) or event-driven (SINGLE rising edge). (IEC 61131-3 Ed.3, §6.8.2 a–b) - If INTERVAL is non-zero, periodic scheduling occurs only while SINGLE is 0. (IEC 61131-3 Ed.3, §6.8.2 b) - If INTERVAL is zero, no periodic scheduling occurs. (IEC 61131-3 Ed.3, §6.8.2 b) - PRIORITY establishes scheduling order with 0 as highest priority and larger numbers as lower priority. (IEC 61131-3 Ed.3, §6.8.2 c; Table 63) - A program with no task association executes once per resource cycle at the lowest priority. (IEC 61131-3 Ed.3, §6.8.2 d) - A function block instance associated with a task executes only under that task, independent of program evaluation rules. (IEC 61131-3 Ed.3, §6.8.2 e)
Scheduling policy (implementer choice permitted by IEC 61131-3, §6.8.2 c): - Deterministic, non-preemptive, fixed-priority scheduling per resource. - Ready tasks at the same priority run in FIFO order by longest waiting time. - Event tasks are edge-detected on the SINGLE input and enqueue one activation per rising edge.
pub struct TaskConfig {
pub name: String,
pub interval: Duration, // INTERVAL; zero disables periodic scheduling
pub single: Option<String>, // SINGLE variable name (event + gating)
pub priority: u32, // 0 = highest priority per IEC 61131-3
pub programs: Vec<ProgramId>,
pub fb_instances: Vec<ValueRef>,
}
pub struct ResourceRunner<C: Clock + Clone> {
runtime: Runtime,
clock: C,
cycle_interval: Duration,
}
impl<C: Clock + Clone> ResourceRunner<C> {
pub fn tick(&mut self) -> Result<(), RuntimeError> {
// single deterministic cycle (tests)
Ok(())
}
pub fn spawn(self, name: &str) -> ResourceHandle<C> {
// start dedicated OS thread
}
}
Implementation notes:
- The SINGLE input is sampled from the current variable state; a transition 0 -> 1 enqueues exactly one activation.
- On task registration, the runtime initializes the previous SINGLE value to avoid a spurious edge on the first cycle.
- Periodic scheduling uses Clock::now() and the task interval (nanosecond Duration).
- Task readiness and elapsed-time accounting use only the injected monotonic
Clock; the exact readiness, discontinuity, and overrun contract is defined
below. Civil wall-clock changes do not make a task ready.
- Inputs are latched at the start of each scheduler cycle; outputs are committed after all ready tasks complete.
- The maximum number of tasks per resource and minimum interval resolution are implementer-specific and are reported by the runtime configuration.
- The resource loop maintains a RUNNING/FAULT/STOPPED state and halts on faults.
Task readiness and overrun accounting¶
For each scheduler sample, the portable readiness boundary observes one
SINGLE value and one logical now. A FALSE-to-TRUE SINGLE transition makes
the task due at now, updates the saved edge state, and does not advance the
periodic baseline or overrun count. A held TRUE value does not retrigger and
suppresses periodic readiness; a subsequent FALSE sample only rearms the next
rising edge. A non-positive interval does not schedule periodic work. Valid
IEC task configuration uses zero for disabled periodic scheduling and a
positive duration for periodic scheduling; a negative portable metadata value
is conservatively treated as disabled. (IEC 61131-3 Ed.3 section 6.8.2 a-b.)
With SINGLE FALSE and a positive interval, a task is not due until now is
at least last_run + interval. A backward logical-clock sample does not move
last_run, create an overrun, or make the task due; periodic readiness resumes
only when a later sample reaches the next deadline relative to the unchanged
baseline. When exactly one interval is due, the returned due time is that
nominal deadline, the missed-interval count is zero, and last_run advances to
the sampled now.
If a forward jump spans n > 1 complete intervals, the scheduler emits one
activation rather than replaying work. Its due time remains the first nominal
deadline, the per-sample missed count is n - 1, the cumulative overrun count
adds that value with saturation, and last_run advances to now. Deadline
addition also saturates at the representable signed-nanosecond bound. Event
readiness never changes those periodic counters. These due-time and accounting
rules are truST bookkeeping beyond the IEC task-trigger definition; they make
host-pause, simulation, and test-clock behavior deterministic. They are
specified here as a truST runtime contract, not as an IEC deviation.
Resource-runner cycle transaction¶
One resource tick samples the current injected clock and task inputs, then executes each due periodic or rising-edge event program once in the deterministic scheduler order. In the reviewed combined case, the initial zero-time tick executes neither program, the ten-millisecond tick executes the periodic program and one rising-edge event activation, and the following ten-millisecond tick executes only the periodic program after the event input falls.
A backward manual-clock sample does not replay periodic work or create an overrun. A later sample that reaches the next deadline resumes from the unchanged periodic baseline. A forward jump spanning multiple complete intervals executes the periodic program once and records the remaining intervals as missed, as defined by the readiness contract above.
If due task execution returns an error, the tick returns that typed error and
latches the runtime fault state. A later tick rejects as
RuntimeError::ResourceFaulted until an explicit recovery transition clears or
replaces that state. This focused contract does not assert rollback of work
completed before the fault.
For the same source, initial runtime state, manual-clock samples, and input
transitions, the ordered RuntimeEvent vector is reproducible. Event payload
debug formatting, wall-clock timing, OS-thread scheduling, and cross-version
trace compatibility are outside this equality contract.
On Unix, the runtime's graceful-shutdown signal set is exactly SIGINT and
SIGTERM. Signals outside that set are not translated into a runtime stop by
the installed signal source. Their operating-system disposition is outside
this runtime contract.
6.3 Timer System¶
Implements IEC 61131-3 timers: TON (on-delay), TOF (off-delay), TP (pulse).
All timers use Clock::now() for elapsed time calculation. Timer instances are evaluated when their owning program or task-associated function block executes; no background threads or interrupts are required.
The normative TP, TON, TOF, and *_LTIME scan-step state machines are defined
in Standard Function Blocks, section 5.
The implementation-owned clock, call, preset, and restart boundaries are
recorded in docs/IEC_DECISIONS.md. Timer traces observe executed calls only;
they do not claim that outputs change continuously between scan steps.
Timer Restart and Time-Base Contract¶
A newly constructed runtime starts its monotonic time at zero. An in-process warm or cold restart does not create a new clock epoch: it preserves the runtime's current monotonic time while reinitializing non-retained timer instances and task timing baselines at that value. The first executed timer call after restart contributes zero elapsed time. Subsequent elapsed time is measured from the preserved post-restart baseline, so a restart cannot make the runtime clock move backward or charge pre-restart elapsed time to a new timer instance.
6.4 Process Image¶
Memory-mapped area for inputs (%I), outputs (%Q), and markers (%M).
pub struct IoInterface {
inputs: Vec<u8>,
outputs: Vec<u8>,
memory: Vec<u8>,
}
Sizes are derived from compiled program metadata at load time. On embedded targets, static sizing may be used, but the logical model remains the same.
The process image is owned by a single resource thread; no internal locking is required. Cross-resource data sharing is synchronized through the configuration-level shared globals lock (see 6.7). External I/O exchange (Modbus, etc.) reads/writes to this image at cycle boundaries.
Process-image bounds, reads, snapshots, and metadata application¶
Each %I, %Q, and %M byte area has an independent maximum length of
16 MiB. A resize, bytecode metadata declaration, source binding, or concrete
write whose addressed window exceeds that limit is rejected before allocating
or mutating the affected area. A failed metadata application is transactional:
resource selection, task/program references, all three area lengths, and I/O
bindings are validated before the executable state or process image changes.
The exact-resource and single-legacy-RESOURCE selection rules are defined in
section 6.7.1.
Reading an unallocated byte or the unallocated portion of a multi-byte value returns zero bytes without growing the area. An unallocated fixed-width string window therefore reads as an empty string. A fixed-width string write whose payload is larger than its declared byte window returns overflow before allocation or partial mutation.
For an in-range concrete address, a successful typed write followed by a typed read of the same address returns the written value. Bit access updates and reads only the addressed bit within its byte. Word access uses the process image's little-endian byte representation and returns the same 16-bit value.
An I/O snapshot is produced after output and marker bindings have been
committed for the cycle. Each entry preserves its area, complete address,
optional display name, optional driver/source description, and optional
declared type. Typed snapshot values are decoded according to that declared
type, including REAL, TIME, and fixed-width STRING; a conversion failure
is represented as an entry error rather than a fabricated untyped value.
Process-image interface and binding transaction¶
A new process-image interface has empty %I, %Q, and %M byte areas, no
bindings, and no hierarchical values. Resizing validates all three requested
area lengths before changing any area. Shrinking truncates only the removed
suffix; growing preserves the retained prefix and zero-initializes every new
byte. A failed resize leaves all three areas byte-for-byte unchanged.
Concrete bit, byte, word, double-word, and long-word access uses the selected area and byte offset. Multi-byte values are little-endian. Bit writes preserve the other seven bits in the addressed byte. A successful write changes no other area. A wrong value kind, wildcard address, zero-length byte-string window, overflowing address calculation, or window beyond the 16 MiB cap is rejected before allocation or mutation.
A fixed-width string write accepts valid UTF-8 whose byte length does not exceed the declared nonzero window. It writes the payload then zero-fills the remaining window, removing bytes from a previous longer value. Reading stops at the first NUL byte; invalid UTF-8 before that terminator is a type error. Unallocated portions of an in-range read are zero-filled conceptually without growing the process image.
Hierarchical addresses are stored outside the flat byte areas and are keyed by their complete area, size, bit, and path identity. Reading an absent hierarchical key fails rather than fabricating zero. Writing one hierarchical key does not allocate a flat process-image byte or alter another hierarchical key.
Bindings retain insertion order. Snapshot rows are partitioned into input, output, and memory lists while retaining insertion order within each area. Named bindings use the declared name as their display name; reference bindings remain unnamed unless an explicit display name was supplied. Recomputing binding provenance replaces every row's previous source with the callback result, including clearing a prior source when the callback returns none. A wildcard binding appears as unresolved and is never read as a concrete zero.
Start-of-cycle input synchronization is transactional across every eligible
%I and %M binding. The interface first reads and type-checks every source
value and resolves every destination. Only after the complete batch succeeds
are values committed to program storage. A read, conversion, undefined target,
or null-reference failure leaves all bound program values unchanged. %Q
bindings are ignored by this phase.
End-of-cycle output synchronization has the symmetric all-or-nothing rule for
eligible %Q and %M bindings. It resolves and converts every program value
into a staged process image before replacing output, memory, or hierarchical
state. Any missing value, null reference, conversion failure, bounds failure,
or type mismatch leaves all three interface states unchanged. %I bindings
are ignored by this phase.
Typed floating-point egress¶
At end-of-cycle synchronization, typed %Q and %M bindings declared as
REAL or LREAL accept only values that are finite at the declared width. A
value converted to REAL must also remain finite after basic-single
narrowing. The runtime preflights every eligible output and marker binding
before mutating either process-image area; any rejected value returns an
IoDriver error, leaves the entire pending %Q/%M image unchanged, and
prevents the normal driver-output commit. The runtime does not clamp,
normalize, substitute, or emit non-finite IEEE bits. Configured safe-state
handling after the resulting cycle fault is a separate policy boundary.
6.5 I/O Drivers¶
I/O exchange is explicit and deterministic: inputs are read into the input image at the start of each resource cycle, and outputs are written after all ready tasks complete.
Marker bindings (%M) are synchronized with program storage at both cycle boundaries:
- Start of cycle: %M process image -> bound variables (same phase as %I input latch).
- End of cycle: bound variables -> %M process image (same phase as %Q output commit).
pub trait IoDriver: Send {
fn read_inputs(&mut self, inputs: &mut [u8]) -> Result<(), RuntimeError>;
fn write_outputs(&mut self, outputs: &[u8]) -> Result<(), RuntimeError>;
fn health(&self) -> IoDriverHealth { IoDriverHealth::Ok }
}
Multiple drivers may be composed (e.g., fieldbus + simulated I/O). The resource scheduler owns the driver(s) and invokes them at cycle boundaries.
Composed drivers retain their configured order. On a successful boundary, each
driver reached by the composition receives exactly one input-read attempt
before task execution and one output-write attempt after task execution. An
earlier driver error short-circuits that boundary. %I and %M bindings are
loaded before ready tasks run; %Q and %M bindings are committed after those
tasks finish; the cycle I/O snapshot is emitted only after that commit. Arrays,
structures, and function-block instance bindings use their flattened declared
layout. An unresolved %I*, %Q*, or %M* binding must be resolved by a
matching-area VAR_CONFIG entry before runtime construction and is rejected
in disallowed declaration sections or on an area mismatch.
The built-in driver registry exposes the stable canonical names ethercat,
gpio, loopback, modbus-tcp, mqtt, and simulated in sorted unique
order. Accepted aliases resolve to the canonical name before validation or
construction; this includes sim/noop, modbus_tcp, mqtt-tcp, and
ether-cat/ecat. The explicit driver name none selects no driver.
Driver lookup trims surrounding whitespace and is ASCII-case-insensitive.
Unknown and empty names are configuration errors. none is reserved and
cannot be replaced by a registered driver.
Registry validation and construction are separate boundaries. Validation
invokes the selected driver's parameter validator only; it must not construct
a driver, open a device/socket, start a worker, or perform a protocol
connection. Construction runs only after successful validation and returns the
canonical name in the built specification. A registered alias cannot shadow a
canonical name, and an alias whose target is absent does not create a usable
entry. Canonical enumeration never includes aliases, empty names, or none.
Operator-facing I/O provenance uses the same canonicalization and alias table
as driver selection. Internal %M bindings always report Internal memory.
With no enabled driver, an external binding has no asserted source. With
exactly one enabled driver, its protocol-specific source is shown; disabled
drivers do not affect that choice. With two or more enabled drivers, the
conservative label is Multiple I/O drivers because whole-image composition
cannot assign one driver to the row. Modbus labels use the direction-specific
configured base plus the addressed word offset, MQTT labels use the
direction-specific topic, and GPIO labels use the addressed bit number.
Provenance is a configuration projection and never claims a connection,
successful exchange, or healthy hardware.
The GPIO driver accepts chardev/libgpiod (defaulting to
/dev/gpiochip0) and the compatibility sysfs backend (defaulting to
/sys/class/gpio). Input entries require concrete %IX bit addresses and
output entries require concrete %QX bit addresses; wildcard, non-bit,
wrong-area, and nested addresses make driver construction fail. The current
construction order does not claim that backend configuration is side-effect
free before this mapping rejection. GPIO read or write failure sets faulted
driver health with the original error. A configured watchdog/fault safe-state
output is written through the GPIO driver and is successful only under the
safe-state confirmation rule below.
GPIO parameters themselves follow these fail-closed rules:
- the parameter root is a table; absent
inputsoroutputsmeans an empty list, while a present list and every list entry must have the declared TOML shape; - the backend name is ASCII-case-insensitive: absent,
chardev, andlibgpiodselect the character-device backend, whilesysfsselects the compatibility backend; no other name is accepted; - every entry has a string
addressand a nonnegativeu32physicalline;pinis a compatibility alias used only whenlineis absent; invertand outputinitialdefault to false and accept booleans, integer zero/nonzero, or stringstrue/false/1/0;- input
debounce_msdefaults to zero and accepts a nonnegative integer or unsigned decimal string; and - one physical line may appear only once across all inputs and outputs, and one process-image bit may appear only once within its input or output list.
Parameter validation performs all mapping-shape and collision checks without opening a GPIO device or exporting a sysfs line. A simple bit address maps byte and bit directly, with bit numbers restricted to 0 through 7. Internal process-image helpers return a configuration error for an out-of-range byte or bit and never panic; writes change only the selected bit.
On input handoff, entries are evaluated in declaration order. Each raw value is
inverted when configured, then debounced, then written to its %I bit without
changing other bits. The first debounced sample establishes state. A change
before the configured delay retains the prior state; a change at or after the
delay is accepted. On output handoff, the selected %Q bit is inverted when
configured and sent only when it differs from the last successfully written
level. Failed writes are not memoized and are retried on the next handoff.
Any backend, process-image bounds, or malformed sysfs value failure
short-circuits the handoff and records faulted health containing the original
error. A later complete successful handoff restores healthy status. Sysfs input
values are exactly trimmed 0 or 1; other content is rejected rather than
being interpreted as energized. Synthetic filesystem tests prove parsing and
sysfs file semantics only. They do not prove pin ownership, voltage, wiring,
kernel driver operation, or device-in-the-loop behavior; those claims require
the separately reviewed real-hardware case and artifact.
Process-image drivers must keep scan-cycle methods bounded. Drivers with
blocking wire protocols may own background workers, but the IoDriver boundary
remains the cycle handoff: read_inputs copies the latest worker snapshot or
returns the configured policy result, and write_outputs hands off the latest
desired output without waiting for protocol round trips. Worker health is
projected through the existing IoDriverHealth surface rather than a parallel
status model. Output handoff is level/latest-value semantics for %Q, not an
edge or pulse delivery guarantee.
Modbus default-fault and deadline integrity¶
The Modbus background worker uses a fixed scan-side deadline for input refresh
and output handoff. Missing that deadline applies the configured on_error
policy at the synchronous IoDriver boundary:
- With
fault, an incomplete input refresh setsFaulted, recordsmodbus input refresh pending, returnsIoTransport, and leaves the caller's input bytes unchanged even when an older accepted snapshot is cached. - With
warnorignore, the same incomplete input refresh setsDegraded, records the pending-refresh diagnostic, and returns success. If an accepted snapshot exists, it may be copied according to the normal snapshot-shaping rules; absence of a snapshot does not invent input bytes. - With
fault, an output handoff that remains incomplete at the deadline setsFaulted, recordsmodbus output handoff pending, and returnsIoTransport. Withwarnorignore, it setsDegradedand returns success. - A timed-out output was already accepted into the asynchronous level/latest handoff. Deadline reporting does not cancel that pending or in-flight output; a later worker result governs its eventual transport outcome.
The positive controls for this deadline contract are a completed default register loopback through the worker and a runtime safe-state handoff confirmed by the worker and observed at the Modbus peer. Cold-start coverage must exercise all three policies before any first snapshot exists and must leave caller bytes unchanged when no accepted snapshot is available.
These deadline rules do not replace the separate policy projection for a completed transport operation. They define only the scan-side result while the requested worker sequence remains incomplete. This is a truST Modbus runtime contract outside IEC 61131-3, so it is neither an IEC decision nor an IEC deviation.
Driver error handling is configurable per driver:
- fault: return an error and fault the resource.
- warn: keep the resource running; driver health becomes degraded.
- ignore: keep the resource running; error is suppressed (health may still degrade).
Driver health is exposed via ctl status and the TUI.
Safe-state handoff confirmation¶
Ordinary scan-output writes use the configured driver error policy and the level/latest-value worker handoff described above. Applying a configured safe state has a stronger success boundary:
- The runtime first applies the configured safe values to the output process image and then attempts the resulting output image on every configured I/O driver.
- A driver's safe-state attempt is confirmed only when
write_outputsreturns successfully and the driver's immediately observed health isOk.DegradedorFaultedhealth means the physical handoff is unconfirmed, even whenon_error = "warn"or"ignore"made the ordinary driver call return successfully. - A worker-backed driver whose handoff is queued, reconnecting, timed out, or otherwise pending must therefore return a safe-state error at the runtime boundary. A later asynchronous delivery does not retroactively turn that failed attempt into a confirmed safe state.
- The runtime attempts the safe-state write on the remaining configured drivers after one driver fails, but returns an error for the overall operation. The first failure identifies the affected driver and cause.
- A deliberate stop with an unconfirmed configured safe state ends in
Faulted, notStopped, and exposes the failure throughlast_error. When safe-state application is part of fault handling, the original root fault is retained and the reported error/event identifies the safe-state failure.
This confirmation rule does not add an unbounded protocol wait. Worker-backed drivers retain their existing bounded handoff deadline; exceeding that deadline is an unconfirmed safe state. With an empty safe-state configuration, the runtime still performs no physical output write as specified in section 6.6. When no I/O driver is configured, applying values changes only the in-memory process image and makes no physical-delivery claim.
The OPC UA client transport-to-worker event handoff is bounded. When its fixed-capacity queue is full, publishing an additional sample, connection, or session event returns failure immediately rather than blocking or allocating an unbounded queue. After the worker drains pending events, the same sink can accept a later event. A rejected event is not claimed delivered.
OPC UA server surface, security, and cold start¶
The runtime's OPC UA server publishes PLC snapshot variables with CurrentRead access only. The periodic publisher refreshes those values from runtime snapshots; client writes are rejected because no transactional write-back path to PLC storage is defined. A future writable server surface requires an explicit allowlist, authorization, type validation, scan-boundary application, and visible write result before it may advertise CurrentWrite.
The server value projection is closed over BOOL; signed 16-, 32-, and 64-bit
integers; unsigned 16-, 32-, and 64-bit integers; REAL; LREAL; and
STRING. Each value retains the corresponding OPC UA Boolean, integer, Float,
Double, or String scalar. A declared enum exports its selected variant name as
an OPC UA String. Null, references, time values, arrays, structures, and other
unsupported runtime values are omitted rather than invented as another scalar
type.
The secure server profile defaults to Basic256Sha256,
SignAndEncrypt, and anonymous access disabled. Policy None is valid only
with message mode None; a signing or encryption policy requires a compatible
non-None mode. Configured authentication and certificate-trust rules are
enforced by the wire server before a client may read a node.
Server construction and listener startup do not depend on the first runtime snapshot. Before a snapshot exists, nodes have no fabricated ready value. After a snapshot becomes available, a reference client may read every exported supported scalar with CurrentRead access. Loopback/reference-client proof establishes interoperability for the exercised profile only; it does not establish external-vendor or production-certificate interoperability.
Floating-point boundary admission policy¶
IEC 61131-3 Ed.3, Section 6.4.2.1, Table 10 defines REAL and LREAL
using the IEC 60559 basic single- and double-width formats and leaves results
involving infinity or not-a-number implementer-specific. Section 6.6.2.5.15,
Table 39 defines IS_VALID so program logic can distinguish finite values from
NaN and infinity. Those provisions do not define how an external host or
protocol admits a value into a PLC process image. truST therefore applies the
following fail-closed product contract:
| Boundary | Typed non-finite input | Required rejection effect |
|---|---|---|
Typed %I snapshot |
REAL/LREAL IEEE bits decode to NaN or either infinity |
Return an I/O error before exposing a typed snapshot value; do not normalize or substitute a value. |
Modbus input_points |
An f32 register value, or its scaled engineering value, is non-finite |
Reject the complete mapped read before changing any byte of the caller's input image. Previously accepted process-image bytes remain unchanged. |
MQTT input_points |
An f32 text, JSON, or binary payload, or its scaled engineering value, is non-finite |
Reject the complete received payload batch before changing the caller's input image or the driver's last accepted mapped snapshot. A later valid payload must not expose values from the rejected batch. |
| OPC UA client points | A configured Float/REAL or Double/LREAL sample is non-finite |
Mark the point faulted and retain the previous PLC value, as specified below. |
| ADS client/server points | A scalar or array REAL/LREAL value is non-finite |
Reject before cache acceptance, write queuing, or PLC storage mutation, as specified below. |
| Runtime mesh subscription | Numeric conversion to the configured REAL/LREAL target is non-finite |
Reject before queuing or applying the local update; retain the previous PLC value. |
hmi.write |
Text or numeric input for a declared REAL/LREAL target is non-finite or overflows the declared width |
Return a failed response with error_code = "runtime_non_finite_value" before queuing the write or changing PLC storage. |
| Managed local debug write/force | Input for a declared REAL/LREAL address is non-finite or overflows the declared width |
Return a failed DAP response and preserve both value and force state, as specified in 6.9.1. |
| File-backed retain save/load | A scalar, array element, or structure field is non-finite | Reject the complete save or load transaction, as specified in 6.7. |
simulation.toml coupling threshold |
The configured threshold is non-finite | Reject the configuration before activation, as specified in 6.9.2. |
For a declared REAL, validation occurs after any scaling or narrowing to IEC
basic single width; a finite wider intermediate that becomes infinite at that
width is rejected. LREAL is validated at basic double width. Rejection never
clamps, normalizes, or substitutes a default. Error-policy handling may decide
whether the resource faults or reports degraded health, but it cannot turn the
rejected value into an accepted process value.
Raw Modbus register mode, raw MQTT byte topics, and integer-address control
operations carry untyped bits and cannot classify an IEEE payload by
themselves. They do not bypass this contract: once those bits cross a declared
REAL/LREAL binding, the typed process-image snapshot or egress preflight
must reject a non-finite representation. This boundary policy does not change
in-process IEC arithmetic, IS_VALID, subnormal, or signed-zero semantics.
Built-in drivers
- Modbus/TCP
- Default profile uses input registers (FC04) for input image.
- Default profile uses multiple holding-register writes (FC16) for output image.
- Explicit
io.tomloptions can selectread_coils(FC01),read_discrete_inputs(FC02),read_holding_registers(FC03),read_input_registers(FC04),write_single_coil(FC05),write_single_register(FC06),write_multiple_coils(FC15), orwrite_multiple_registers(FC16). - Optional
input_pointsandoutput_pointsmap individual coil/register addresses to process-image offsets withbool,u16,i16,u32,i32, orf32types, linearscale/offset, and Modbusbyte_order/word_order. - Register payloads are big‑endian (high byte first).
- Coil payloads are Modbus-packed with the first process-image bit in the least-significant bit of the first byte.
- Point-map numeric values are stored in the runtime process image as
little-endian bytes after scaling so
%IW/%IDbindings read the same value that the map produced. - Register quantity is derived from the process image size (
ceil(bytes / 2)). - Runtime exchange is worker-backed: scan-cycle reads/writes use bounded snapshot/handoff state while Modbus TCP connect/read/write round trips happen on the Modbus worker.
- A completed worker read preserves its typed protocol error at the scan-cycle
boundary. In particular, a Modbus exception remains an
IoAddresserror and is not replaced by a generic unavailable-snapshot transport error. - Function selection is closed over FC01, FC02, FC03, and FC04 for input and FC05, FC06, FC15, and FC16 for output. An unknown or direction-incompatible function is rejected during configuration.
- A coil point is boolean and does not accept numeric scaling. A register point uses a declared supported numeric type; scaling must be finite and invertible for the requested direction. Invalid point type, address, function, or scaling is rejected before the worker starts.
- A completed transport failure follows
on_error:faultreturns the typed error and faults health, whilewarnandignorereturn degraded success. This policy never rewrites a Modbus exception into a generic transport error. - Reconnect uses a non-zero bounded backoff and cannot spin on an unavailable endpoint. Dropping the driver requests worker shutdown and returns within the bounded driver-drop deadline even when the worker is awaiting its first response. This return-time contract does not claim that the detached worker thread has already joined; joined worker shutdown remains open proof debt.
Modbus declared-width floating-point finiteness¶
For an f32/REAL point, finiteness is checked again after scaling or inverse
scaling is narrowed from the internal f64 calculation to the declared
f32 width. A finite f64 result that becomes non-finite at f32 width is
rejected before process-image mutation or Modbus wire encoding; it must not
become an infinite REAL value or register payload. This is a truST Modbus
product and safety contract outside IEC 61131-3, not an IEC decision or
deviation.
Typed protocol point-map integration contract¶
Typed point maps bind one protocol scalar to one bounded process-image scalar. They do not reinterpret arbitrary trailing bytes, merge overlapping definitions, or use a partially converted batch as PLC input.
The common scalar set is bool, u16, i16, u32, i32, and f32.
Documented IEC-style aliases (boolean, word, int, dword, dint, and
real) and explicit-width aliases select the same type case-insensitively.
Integer conversion rounds to the nearest integer with half-way values away
from zero, then checks the declared range. F32 conversion must remain finite
after narrowing. Inbound scaling is engineering = raw * scale + offset;
outbound scaling is raw = (engineering - offset) / scale. Scale must be
finite and non-zero and offset must be finite. Boolean points do not accept
scaling.
Process-image numeric values are little-endian and occupy exactly two or four
bytes according to their declared type. A Boolean occupies one selected bit;
its bit index is 0..=7, and writes preserve every other bit in the byte.
Every offset-plus-width calculation is checked for arithmetic overflow and
image bounds before mutation. A failed conversion or range check leaves the
complete destination bytes unchanged.
Within one input or output map, two point definitions must not claim the same
protocol identity or overlapping process-image storage. Boolean bits in the
same byte are distinct storage only when their bit indices differ. Modbus
register ranges account for the type's complete one- or two-register width and
the selected function/table. MQTT protocol identity is specified by
32-mqtt-io.md.
An input and output may use the same process-image offset because %I and
%Q are separate images. Ambiguous duplicates or overlaps are configuration
errors before a worker or transport is started.
MQTT is a communication protocol owned by
32-mqtt-io.md. This runtime-engine document owns only its
integration boundary: startup resolves symbolic tags and installs all
process-image bindings atomically before the protocol worker starts; the worker
cannot access program storage or resolve symbols; scan-cycle calls exchange
bounded snapshots and handoffs; and runtime I/O snapshots retain the declared
PLC type metadata required by control and debug surfaces.
For Modbus:
- FC01/FC02 points are Boolean and FC03/FC04 points are numeric; FC05/FC15 outputs are Boolean and FC06/FC16 outputs are numeric;
- omitted type defaults to Boolean for bit functions and U16 for register functions;
- FC06 accepts only one-register U16/I16 values;
- byte order applies inside every 16-bit register and word order applies
between the two registers of a 32-bit value; both default to big-endian and
accept the documented
big/beandlittle/lealiases; and - a decoded register scalar consumes exactly the declared register byte count. A truncated or oversized buffer is rejected rather than partially decoded.
These point-map rules are truST protocol/product behavior outside IEC 61131-3. Driver error policy may classify the resulting error, but cannot turn an invalid configuration, malformed scalar, out-of-range value, or partial batch into accepted PLC data.
- MQTT I/O integration
- MQTT configuration, topics, mapping, payload, security, session, discovery,
and interoperability behavior is specified by
32-mqtt-io.md. - The host runtime lowers MQTT points through the shared process image and starts the protocol-owned worker only after configuration and binding succeed.
- Broker connect, poll, publish, and reconnect work never runs on the PLC scan thread. Scan reads and writes use bounded snapshot and handoff state.
- Driver failures enter the common runtime I/O health and fault-policy path.
- MQTT is not part of the PLC executor, scheduler, or IEC language semantics.
MQTT runtime integration integrity¶
MQTT worker deadlines and fault projection are runtime integration contracts.
Broker delivery, MQTT sessions, topics, payloads, and reconnect semantics are
owned by 32-mqtt-io.md. Neither document turns these truST
product contracts into IEC language decisions or deviations.
- EtherCAT (backend v1)
- Driver name:
ethercat. - Deterministic process-image mapping for module-chain profiles (including
EK1100+ digital I/O modules such asEL1008/EL2008). - The default reviewed module-chain profile is
EK1100,EL1008, andEL2008, with deterministic input/output byte lengths and channel order. An explicit hardware adapter name is accepted as configuration authority; it is not proof that the adapter exists. - Startup discovery diagnostics emit discovered module summary and expected process-image sizes.
- Cycle-time health telemetry upgrades driver status to degraded when cycle read/write exceeds configured warning threshold.
- A read or write failure follows the configured fault/warn/ignore policy, except that a discovered process-image size mismatch is always faulted because continuing would reinterpret channel layout. Warn and ignore may keep the resource running only when the image shape remains valid, and must preserve the failure detail in degraded health.
- Non-mock adapters are backed by EtherCrab hardware transport on unix targets.
- Deterministic
adapter = "mock"mode is available for CI/offline validation. -
Explicit v1 non-goals: no functional safety/SIL claims and no advanced motion profile support.
-
OPC UA client floating-point ingress
- A
Goodsample for a configuredFloat/REALorDouble/LREALinput point is accepted only when the value is finite. NaN, positive infinity, and negative infinity are rejected before the sample becomes an accepted cache value or is written to PLC variable storage.- A rejected sample marks the affected point faulted, exposes diagnostic detail, and leaves the PLC target unchanged. The runtime does not clamp, normalize, or substitute a default value.
- A previously accepted finite value may remain visible for diagnostics, but
a faulted point does not apply it as a fresh input. A later finite
Goodsample may recover through the normal subscription path. - This rule is limited to OPC UA client scalar ingress. Scalar OPC UA egress is governed by the worker/cache integrity contract below. Arrays, structures, subnormal values, signed zero, and other protocol/API/retained ingress boundaries require their own reviewed contracts.
OPC UA client worker, cache, and scalar-output integrity¶
This is a truST OPC UA product contract outside IEC 61131-3. IEC defines the
PLC data types exposed at the runtime boundary, but it does not define OPC UA
session generations, subscription event authority, write acknowledgements, or
worker shutdown. These rules therefore belong here, not in
IEC_DECISIONS.md or IEC_DEVIATIONS.md.
The client cache is closed over the configured bindings. A subscription sample from the active session may mutate cache or PLC input authority only when its variable name, node ID, declared OPC UA data type, and access mode exactly match one configured readable point. A current-session sample with an unknown or mismatched identity is a visible validation fault and must not create a dynamic point status, replace a configured point's value, or write PLC storage.
Every connection or recovery candidate has a monotonically increasing local session generation, and every callback sink created for that candidate carries that generation. Only events from the active generation may change connection, point, value, or freshness authority. A delayed sample, disconnect, or session-closed callback from an older generation is ignored and cannot regress a newer connected session. Within the active generation, accepted event time and point freshness never move backward; an older event cannot replace a newer value or connection/point state.
Each locally captured output intent carries a monotonically increasing write generation. Capturing a changed finite value atomically replaces the queued value for that point. A transport completion may remove the queued value or change its point quality only when the completion still names the exact generation that was transmitted. Completion of an older value cannot remove a newer queued value or overwrite its pending or error quality. A correctly correlated validation rejection terminally removes that exact generation, faults the affected point visibly, and does not reconnect the otherwise healthy session.
For scalar Float/REAL and Double/LREAL outputs, the value must be finite
at the declared width before it enters the pending-write cache. A non-finite
output cancels any older queued intent for that point, advances the local write
generation, reports an explicit point fault, and is never sent to the
transport. The queued-value baseline is cleared so a later finite value can be
queued normally. The runtime does not clamp, normalize, or substitute a
default.
At the OPC UA wire adapter boundary, a write response is successful only when
it contains exactly one status for every request, in request order, and every
status is Good. A missing, extra, or non-good result cannot be
reported to the worker as a successful batch. This is a fail-closed truST
adapter rule; it does not claim that IEC 61131-3 defines OPC UA service-result
semantics.
Worker shutdown or drop must wake an idle interval wait promptly rather than sleeping for the configured tick interval, attempt transport disconnect, make the shared connection and readable-point authority non-good, and join the worker thread. Raw values may remain available for diagnostics. This local wakeup guarantee does not claim cancellation of a transport call already in flight.
Whether pending outputs replay after reconnect and how server source timestamps are ordered remain unresolved product decisions. This contract does not provide authority for tests that select those behaviors.
OPC UA client configuration, trust, and one-shot operation integrity¶
This is a truST OPC UA product and security contract outside IEC 61131-3.
Configuration, certificate stores, endpoint discovery, browsing, and OPC UA
service-response vectors are not IEC language semantics and therefore do not
belong in IEC_DECISIONS.md or IEC_DEVIATIONS.md.
An OPC UA client configuration contains at least one connection. Connection
names are trimmed, non-empty, and unique across the complete configuration.
Every connection has a trimmed non-empty opc.tcp:// endpoint with a
non-empty authority and at least one point. Point variable names are unique
across all connections; variable and node identifiers are trimmed and
non-empty. Node-ID grammar remains a wire-boundary validation because the
parser must remain available when OPC UA wire support is not compiled.
The parser defaults omitted client security policy and mode to none/none.
Policy and mode spelling is trimmed and case-insensitive, with - and _
ignored while matching. It accepts only none/none, basic256sha256 with
sign or sign_and_encrypt, and aes128sha256rsaoaep with sign or
sign_and_encrypt. Authentication defaults to anonymous. Anonymous
configuration rejects the presence of either username or password, including
blank values. Username authentication requires both trimmed non-empty fields.
Errors must not echo password contents.
Client polling is at least 10 ms. Timeout is in the closed interval from 1
through 60,000 ms. Every point declares one supported scalar type. The accepted
type spellings are bool/boolean, int/int16, dint/int32,
lint/int64, uint/uint16, udint/uint32, ulint/uint64,
real/float/float32, lreal/double/float64, and string,
matched after trimming and ASCII case folding. access and the legacy
writable alias are mutually exclusive rather than order-dependent; omission
means read access. Access accepts read, write, and
read_write/readwrite/read-write, with the same trimming and case-folding
rule. Read and read/write points are supported, while write-only points are
rejected because the runtime requires read evidence before reporting green
status.
Certificate listing, clearing, and explicit rejected-certificate promotion are
closed over the configured client PKI directory. They consider only regular
.der, .pem, or .crt files and return deterministic path order. A
symbolic-link listing/source root or entry observed during inspection is
ignored. A symbolic-link trusted promotion root or per-certificate destination
observed before copy is rejected, and that rejected source remains in place.
Clear removes only the listed trusted files. Explicit trust copies only listed
rejected files into the trusted root and removes only those exact rejected
files after each successful copy. Concurrent filesystem mutation between
inspection and file operation, batch atomicity across multiple promoted
certificates, and the destination policy for two different rejected paths with
the same filename remain unresolved and are not test authority.
For one-shot point reads and writes, the service response must contain exactly
one result for every request before any result is reported as successful. A
short or extra read vector is rejected before returning a value prefix. A
short or extra write-status vector is rejected before reporting batch success,
and every position in an exact-length vector must be Good. Result order is
interpreted as request order because the service response carries no separate
point identity at that boundary.
Client error classification is stable and ordered. Certificate failures map
to cert_untrusted; browse access, readability, and unknown-node failures map
to browse_denied; identity, credential, and security-policy rejection during
activation map to auth_required; absence of a matching supported endpoint
maps to unsupported_security_profile; and remaining transport failures map
to endpoint_unreachable. Classification changes recovery guidance only and
must not expose credentials.
- ADS floating-point ingress
- ADS
REAL/LREALvalues entering through client reads/notifications or ADS server writes are accepted only when every scalar value is finite. The same rule applies to eachREAL/LREALarray element. NaN, positive infinity, and negative infinity are rejected by the typed ADS decoder before a client sample is accepted, a server write is queued, or PLC variable storage is modified.- A rejected client sample reports point-data error quality and leaves the previous PLC target unchanged; it does not require the ADS session itself to disconnect. A rejected server write returns invalid-data status and queues no write.
- ADS client egress applies the same finite-value requirement before a value enters the shared write queue or reaches the wire. A rejected scalar or array output remains unchanged in PLC storage, cancels any older local pending intent for that point, advances its local write generation, reports explicit error quality, and clears the queued-value baseline so a later finite value can be queued normally. An ADS operation already in flight cannot be cancelled, but its completion cannot overwrite the newer rejection state.
- The runtime does not clamp, normalize, or substitute a default value. Subnormal values and signed zero are outside this rule.
Connector status and discovery truth contract¶
Connector reporting is an additive supervisory contract over protocol-owned
transport loops. It does not replace IoDriver, ADS, OPC UA, Modbus, MQTT, or
EtherCAT execution. Every connector report uses the following closed
vocabularies:
- lifecycle state:
disabled,configured,starting,ready,degraded,reconnecting,stale,not_ready, orfaulted; - health:
ok,degraded,faulted, orunknown; - discovery confidence:
confirmed,likely,port_reachable, orunavailable; - point quality:
good,stale,bad,unsupported,unavailable,write_pending, orwrite_failed.
ready with ok health requires current protocol or driver evidence.
Configuration, a starting worker, a listener without a runtime snapshot, and
transport reachability do not satisfy that requirement. They remain
configured, starting, not_ready, or unknown as applicable. A connector
is stale when the whole connection is no longer fresh. A single stale point
uses point quality stale; the connector may remain usable but must report
degraded point counts instead of collapsing the point and connector states.
ADS reports without a usable runtime snapshot are not_ready/unknown.
Connected ADS or OPC UA reports are ready/ok only with no degraded points;
otherwise they are degraded/degraded. Reconnecting, stale, disabled, and
faulted source states retain their distinct normalized states.
Discovery confidence describes evidence, not desirability or deployment readiness:
confirmedrequires a valid protocol-level exchange. Modbus uses FC43/14 device identification or an explicitly configured FC03 safe read. A normal or protocol-valid Modbus exception response confirms the protocol. MQTT classification is defined by32-mqtt-io.md.likelyrequires a protocol-shaped response that does not establish an accepted session. MQTT rejection classification and authentication evidence are defined by32-mqtt-io.md.port_reachableproves only that the TCP connection succeeded. It must not be rendered or consumed as a confirmed Modbus device or MQTT broker.unavailablemeans that no useful discovery evidence was obtained.
Consumers of this contract must reject unknown or missing state, health, and discovery-confidence values instead of rendering them as healthy. When one fleet peer supplies an invalid connector report, the peer's raw topology remains visible and the validation failure is shown to the operator; the bad report must not make that peer or the other configured peers silently vanish.
MQTT discovery evidence and session-cleanup behavior are specified by
32-mqtt-io.md.
The Modbus discovery wire probe emits a valid MBAP header with protocol ID zero, exact transaction ID, unit ID, and PDU length. FC43/14 uses transaction 1; an optional FC03 safe read uses transaction 2 after a fresh bounded connection. Request PDUs are limited to the Modbus TCP PDU maximum and an empty or oversized PDU is rejected before writing. A response is protocol evidence only when transaction ID, protocol ID, length, unit ID, function, and complete body all match the request. Response length is 2 through 260 bytes; a normal function response is accepted, and an exception function requires an explicit exception code. A mismatched unit, truncated exception, malformed length, unexpected function, or partial body is not Modbus evidence.
MQTT packet validation and CONNACK classification are owned by
32-mqtt-io.md; this shared discovery surface consumes only
the resulting evidence class.
Communication discovery scope and symbol browsing¶
Communication discovery is a bounded authoring operation. A directed Modbus or
MQTT TCP connection reports only port_reachable until the protocol probe
rules above earn stronger evidence. A CIDR scan broader than /24 is rejected
before enumeration. EtherCAT discovery is available only from the runtime host;
another origin receives an empty candidate set and an explicit runtime-origin
warning. Runtime-origin mock discovery may project the configured/discovered
module chain, but it remains mock evidence. OPC UA generic discovery currently
returns an explicit server-setup warning and no candidates. A registered
protocol whose discovery is deferred returns an empty candidate set and a
stable unavailable warning; an unknown protocol is a request error.
comm.discover schema version 1 requires a non-empty string protocol. Its
optional scope is an object, origin is this_host or runtime, and passive
defaults true. Unknown request or scope keys are rejected so misspelled safety
limits cannot silently disappear. A requested non-passive scan still performs
connect/read-only operations and returns the stable warning that active write
probes are unsupported. Timeout defaults to 150 ms and is clamped to the
closed 1 through 2,000 ms range; malformed timeout types are request errors.
Protocol aliases are deterministic: Modbus hyphen/underscore forms select
modbus_tcp; OPC UA server and client aliases remain distinct; MQTT broker
selects mqtt; ADS client and TwinCAT select ads; and truST/runtime/mDNS
aliases select discovery. Deferred registered protocols return their
protocol-specific stable title in the unavailable warning. A blank or unknown
protocol never falls through to a generic registered protocol.
A Modbus directed host accepts a host, IPv4 address, or bracketed IPv6 address
with optional nonzero port and defaults to port 502. Its unit ID defaults to 1.
The optional safe FC03 read requires probe_read_address; quantity defaults to
1 and is limited to 1 through 125. Supplying quantity without an address, zero,
or a value above 125 rejects the request before a connection. A CIDR is trimmed
and parsed as IPv4/prefix; /24 is the broadest accepted range. Enumeration is
ascending and canonicalized to the containing network: /24 through /30
exclude network and broadcast, /31 includes both point-to-point addresses,
and /32 includes its sole address.
MQTT target validation, directed-only scope, deterministic endpoint order, and
TLS evidence limits are owned by 32-mqtt-io.md.
OPC UA client discovery requires a directed host or endpoint. A bare host
defaults to opc.tcp://<host>:4840; an explicit valid port, bracketed IPv6
authority, or path is retained, and a canonical opc.tcp:// endpoint is
trimmed and retained. Other schemes, userinfo, zero or invalid ports, query or
fragment components, and ambiguous unbracketed IPv6 reject before calling Get
Endpoints. Endpoint discovery failure is an explicit warning and empty result,
not a successful fabricated endpoint.
Every candidate ID is deterministic and contains only ASCII alphanumeric, hyphen, underscore, period, and the protocol-owned separators added by the projection. Candidate source and confidence come from the actual observation; labels and parameters do not upgrade TCP reachability into protocol confirmation. Candidate order follows deterministic target/source order, and duplicate observations are collapsed by their protocol identity.
Symbol browsing returns a versioned tree whose protocol, kind, IDs, paths,
type labels, writable flag, and protocol-specific identifiers are derived from
the owning source:
- Local project browsing returns declared globals in deterministic name order and does not claim runtime values.
- EtherCAT channel browsing returns the configured module and channel order with exact process-image direction, byte/bit coordinates, and data type.
- Cached ADS browsing canonicalizes the supplied snapshot and returns the same
deterministic candidate groups and import shape owned by the ADS
symbol-import contract. The target AMS identity accepts the documented
ams_net_idandtarget_net_idaliases. - A live ADS upload failure classified as a missing return route returns a
structured
route_missingerror plus the credential-free route plan. It must not fabricate a symbol tree or successful import. - OPC UA leaf browsing retains the raw node ID and maps a supported scalar data type into the exact apply-facing type name. Browse failure retains the classified protocol error instead of returning an empty successful tree.
comm.browse_symbols schema version 1 requires a non-empty string protocol.
Kind defaults to symbols; protocol and kind are trimmed, ASCII-case folded,
and hyphen-normalized before dispatch. Unknown request or target keys reject.
The supported protocol/kind pairs are ADS symbols, OPC UA client nodes,
EtherCAT channels, and local OPC UA server, ADS server, or OpenOT symbols.
Every other pair is an explicit request error. Local browsing is selected only
for symbols on a local-capable protocol when the target is absent, explicitly
local, or has no remote host; a nonempty remote host never falls through to
project compilation.
Every successful response includes exact schema version, canonical protocol,
canonical kind, and a tree. Empty optional error, route, ads_import, and
warning members are omitted. A structured protocol error contains an empty
tree and no route/import payload. Secrets from targets, cached snapshots, route
plans, and protocol errors never appear in the tree, warnings, or diagnostic
text.
A cached ADS snapshot must use the current snapshot schema, a non-empty trimmed
route name, and valid non-empty dotted symbol paths. Empty segments, leading or
trailing periods, duplicate descriptor identities, and conflicting duplicate
symbol paths reject before tree or import projection. Snapshot order is
canonicalized before both projections. ADS groups and leaves are sorted
lexicographically at every level. Groups carry deterministic path-derived IDs,
type = group, children, and no size or writable claim. Leaves retain exact
remote path, ADS source type, byte size, and a writable flag derived only from
the remote Write capability. The tree and ads_import.snapshot contain the
same descriptor denominator.
An ADS live target accepts ip/host and ams_net_id/target_net_id
aliases. Host and AMS Net ID are trimmed; host is a bare IPv4, IPv6, or DNS
name without scheme, userinfo, path, query, or fragment. AMS Net ID is exactly
six decimal octets in the range 0 through 255. AMS port defaults to 851 and
must be nonzero. Conflicting duplicate aliases reject. Missing target and
snapshot, or an instance ID without the target needed for live browsing,
reject before route or wire activity. Cached browsing performs no route check
or network activity.
OPC UA browsing target, security, authentication, and credential rules are the same as OPC UA client Test. The derived security profile's anonymous flag must agree with the normalized auth mode. Each projected node preserves child order, raw NodeId, browse path, OPC UA type NodeId label, apply-facing scalar data type, and writable flag recursively; only its UI ID is sanitized.
EtherCAT browsing selects a configured EtherCAT driver by its exact
protocol-qualified instance identity, retaining the original io.drivers
index when other protocols precede it. Without an instance it selects the
first configured EtherCAT driver. A stale or cross-protocol identity rejects.
Module and channel order is configuration order; module IDs, paths, model,
slot, channel count, per-channel index, one-byte BOOL type, and ownership are
projected without claiming a live hardware observation.
Reading a project file, generated temporary artifact, cached snapshot, or live wire response remains an external oracle. A structural response-shape assertion may map to this contract, but an unresolved file or wire observation cannot be promoted to proof merely because surrounding assertions are static.
EtherCAT unavailable-resource contract¶
Creating an EtherCAT driver with a non-mock adapter does not require the adapter to be present, so project construction and runtime startup remain non-blocking. The first operation that needs the wire attempts bounded hardware initialization. If the adapter, configuration, or post-allocation hardware resource is unavailable, the operation returns an error and the connector is faulted rather than healthy.
EtherCrab process-data storage cannot be safely allocated repeatedly after a
post-allocation initialization failure. Such a failure is therefore terminal
for that driver instance: later reads and writes return the visible
ethercat hardware unavailable until driver rebuild error without another
allocation attempt or retry window. Recovery requires constructing a new
driver instance. This terminal rule prevents an unavailable adapter from
causing unbounded allocation, retry, or scan-cycle delay. adapter = "mock"
proves deterministic software behavior only and is not evidence that physical
EtherCAT hardware is available or production-ready.
6.6 Fault, Overrun, and Watchdog Handling¶
The runtime traps execution faults and reports them through a unified fault channel. By default, a fault transitions the resource into a FAULT state and halts further task execution until restarted.
Faults include: - Arithmetic errors (e.g., divide by zero) - Out-of-bounds accesses - Invalid type conversions - FOR loops with a step expression that evaluates to 0 (guarded by bytecode and treated as a runtime fault) - Task overruns (missed deadlines)
Overrun policy (default): if a periodic task misses its deadline, the missed activation is dropped, the overrun counter increments, and the task is eligible again on the next interval boundary.
Watchdog policy (production): - A watchdog monitors cycle/task execution time. - If the watchdog timeout elapses, the runtime raises a FAULT and halts the resource. - The output-commit deadline is checked before any physical driver write. If it has elapsed, the pending process-image output is not sent to a driver. - Timeout thresholds, fault actions, debugger interaction, automatic recovery, and the physical driver commit boundary are configured by the truST host runtime (see §6.9). IEC 61131-3 does not define these host mechanisms, so they are product contracts here rather than IEC deviations. - Default action is safe_halt: outputs are set to configured safe values (if provided), then the resource halts. For halt and safe_halt, safe-state outputs are applied before halting. If no safe-state output values are configured, fault handling performs no physical output write; it must not re-send the pending process-image output as a substitute safe state. - If a pre-commit fault occurs after a pending image has been staged, the runtime restores the last physically committed output image before applying configured safe values. A partial safe-state map overrides only its named addresses; every unconfigured output therefore retains its last committed value and cannot leak the failed scan's pending value.
Debug and resource pause interaction: - The watchdog measures active cycle execution, not operator dwell time at a debugger statement-boundary pause. Time spent waiting in that paused state is excluded from the current cycle's execution and output-commit deadlines. - A resource pause accepted between cycles does not arm a new watchdog deadline and does not start another input, task, retain, or output phase while paused. - A debugger pause inside a cycle suspends that cycle. It does not start another scan or commit outputs while waiting. Resume continues the suspended cycle with the same remaining active-execution budget; it does not grant a fresh full watchdog interval. - Pause alone does not apply safe state or clear a fault. A real active-execution timeout before or after the paused interval still follows the configured watchdog fault action.
These pause rules are a truST host/debugger contract outside the IEC language execution model, not an IEC deviation.
Automatic restart policy:
- A non-panic cycle fault configured with FaultPolicy::Restart, or a watchdog
timeout configured with restart action, enters the same warm-restart storage
transition defined in section 6.7: RETAIN values are preserved while
NON_RETAIN and ordinary initialized storage are reinitialized.
- A contained ResourcePanic is never automatically restarted. It remains a
visible fault so an unexpected unwind cannot be hidden by retry.
- Automatic restart attempts are bounded to three consecutive attempts for the
same unresolved fault. Exhaustion leaves the resource faulted with a
diagnosable restart-limit error.
- Consecutive automatic retries use a nonzero monotonic backoff derived from
the configured cycle interval. The base is clamped to the inclusive range
from one millisecond through one second, later attempts increase
exponentially, and the resulting delay never exceeds one second.
Per-cycle deadline lifecycle: - Before executing a cycle with an enabled watchdog, the scheduler arms both the execution and output-commit deadlines from the same wall-clock start and configured timeout. A disabled watchdog leaves both deadlines unchanged. - The scheduler restores the deadlines that existed before the cycle after normal completion, an ordinary runtime error, or a contained cycle panic. Temporary per-cycle deadlines therefore cannot leak into later work. - Enabled non-positive policy values are normalized before installation as specified below. As a defensive helper boundary, a zero timeout that reaches deadline construction produces an immediately expired deadline rather than disabling enforcement or extending the cycle.
Fault-state clear lifecycle¶
The portable fault holder clears its current fault as one state transition:
faulted becomes false and the stored last fault becomes absent. Clearing does
not change the configured FaultPolicy. Calling clear while already healthy is
idempotent and leaves the healthy state, absent last fault, and policy
unchanged.
This clear operation is bookkeeping at the portable holder boundary. It does not restart or resume a resource, apply safe outputs, reset program or retained storage, prove that the underlying fault was repaired, or authorize a caller to recover production execution. Those actions remain owned by the host recovery and control boundaries. This is a truST runtime API contract, not an IEC deviation.
Fault-policy decision projection¶
The portable fault holder projects its configured FaultPolicy to a
FaultDecision using this closed table: Halt returns action Halt with
apply_safe_state = false; SafeHalt returns action SafeHalt with
apply_safe_state = true; and Restart returns action Restart with
apply_safe_state = false. The projection reads only the current policy. Its
result is unchanged by whether the holder is healthy or faulted and by the
presence or value of the stored last fault. It does not mutate any of those
observations.
A FaultDecision is an instruction to the owning host-control path, not proof
that its action occurred. Producing it does not itself apply safe outputs, halt
or restart a resource, clear a fault, reset storage, consume a retry, or
authorize recovery. Those effects remain governed by the safe-state, automatic
restart, and recovery-control contracts above. This is a truST runtime API
contract, not an IEC deviation.
Fault recording and policy replacement¶
Recording a runtime fault sets the portable holder's faulted observation to
true and stores the supplied RuntimeError as the exact last fault. A later
record replaces the earlier last fault. Recording does not change the
configured FaultPolicy, execute that policy, clear state, apply safe outputs,
or restart a resource.
Replacing FaultPolicy stores the supplied policy exactly. It does not change
the current faulted flag or stored last error, including when policy is changed
while the holder is already faulted. Policy replacement returns no evidence
that the corresponding halt, safe-state, or restart action was executed. These
are truST runtime API contracts, not IEC deviations.
Watchdog policy normalization and installation¶
Before an enabled WatchdogPolicy is installed, a timeout less than or equal
to zero is normalized to one millisecond so the scheduler cannot receive an
always-expired enabled deadline. A positive enabled timeout is preserved.
Disabled policy timeouts are inert at this boundary and remain unchanged. The
configured WatchdogAction is preserved in every case.
WatchdogSubsystem::set_policy replaces the previous policy with that exact
normalized value. Installation does not arm a deadline, report a timeout,
apply safe state, halt, or restart a resource; those effects remain owned by
the scheduler and host-control paths. The subsystem decision projection reads
the installed action without mutating the stored policy. These are truST
runtime configuration contracts, not IEC deviations.
Portable watchdog and fault configuration model¶
Configuration token parsing trims surrounding whitespace and compares ASCII
case-insensitively. WatchdogAction and FaultPolicy accept exactly halt,
safe_halt, and restart; RetainMode accepts exactly none and file.
Every other token returns RuntimeError::InvalidConfig. The diagnostic
preserves the complete original, untrimmed text in one of these exact forms:
invalid watchdog action '<text>', invalid fault policy '<text>', or
invalid retain mode '<text>'.
The default WatchdogPolicy is disabled, has a zero duration, and selects
SafeHalt. Both WatchdogSubsystem::new and Default store that policy.
Their decision projects Halt to (Halt, apply_safe_state = true),
SafeHalt to (SafeHalt, true), and Restart to (Restart, false).
The watchdog Halt projection therefore deliberately differs from the
ordinary FaultPolicy::Halt projection: a watchdog timeout requires safe
output handling before halt.
Both FaultSubsystem::new and Default select FaultPolicy::Halt, report a
healthy state, and contain no last fault. Reading policy, decision, faulted
state, or last fault does not mutate the holder. FaultInfo stores the exact
human-readable reason supplied by the host, and cloning it preserves that
text. Constructing or reading these portable model records does not execute a
halt, safe-state write, restart, retain operation, or recovery action.
6.7 Retain Storage (IEC 61131-3 §6.5.6)¶
Retentive variables must follow IEC 61131-3 retentive variable rules (§6.5.6, Figure 9). At startup:
- Warm restart: RETAIN variables restore their retained values; NON_RETAIN are initialized.
- Cold restart: RETAIN and NON_RETAIN variables are initialized.
- Unqualified variables follow the runtime's retain policy (see
docs/IEC_DECISIONS.md). VAR_STATfollows the documented vendor-extension storage rules: function statics persist across calls, method statics persist per instance and per method, andPROGRAM/FUNCTION_BLOCK/CLASSVAR_STATuses ordinary instance storage.
Retain storage is provided via a pluggable backend:
pub trait RetainStore: Send {
fn load(&self) -> Result<RetainImage, RuntimeError>;
fn store(&self, image: &RetainImage) -> Result<(), RuntimeError>;
}
Retain snapshot identity and order¶
The portable retained snapshot receives already-resolved logical names. It does not canonicalize, case-fold, or otherwise reinterpret a supplied key. The first insertion of a name appends one entry after all existing entries. Inserting the same resolved name again replaces only that entry's value at its original position: snapshot cardinality, sibling values, and sibling order do not change. A different name remains a distinct entry and appends in call order.
Configuration/global retained variables keep their resolved logical name.
Retentive variables declared inside a PROGRAM use the reserved internal
identity @program/<program-identity>/<variable-identity>. The @program/
prefix cannot be an IEC identifier, so it cannot collide with a declared
global. Program and variable identities retain their reviewed spelling in the
file and resolve ASCII-case-insensitively on load. Snapshot creation visits
programs in runtime registration order and variables in declaration order.
Program-variable entries participate in the same full-snapshot validation,
migration, orphan reporting, and atomic apply boundary as retained globals.
Insertion stores the complete runtime Value without changing its concrete
variant or nested contents. This is a deterministic snapshot-construction
contract: constructing a snapshot from an ordered map, borrowing its values,
and consuming it back into a map preserve the exact key spelling, entry order,
runtime value variants, and nested values without normalization or
reconstruction.
contract, not a validation bypass: declared-type migration, non-finite value
rejection, serialization, and atomic application remain owned by the retain
save/load/apply boundaries below. Successful insertion alone does not mean a
snapshot is valid for persistence or warm reload. IEC 61131-3 Ed.3 section
6.5.6 defines retentive variable behavior but not this internal container
policy; the truST choice is specified here and is not an IEC deviation.
The runtime loads retained values during resource startup and writes them on shutdown and periodically (policy defined in the runtime configuration). The periodic cadence is rate-limited and only writes when retained values have changed.
The file-backed retain boundary accepts REAL and LREAL values only when
they are finite. This rule is recursive through retained arrays and structures
and applies to both serialization and deserialization, including legacy retain
images. A rejected save does not replace the last durable snapshot. A loaded
snapshot containing NaN, positive infinity, or negative infinity is rejected
as a whole before any retained global is applied; the runtime does not clamp,
normalize, or substitute a default value. In-process floating-point execution
outside the file-backed retain boundary is unaffected by this rule.
Retain-store failures use the following fail-closed transaction policy. IEC 61131-3 section 6.5.6 defines which variable classes are retentive across a restart; the storage and failure boundaries below are the truST runtime contract:
- A missing configured file represents an empty snapshot. Open, read, framing, checksum, decode, declared-type migration, and value-compatibility failures are errors. A warm startup or warm reload that encounters one of these errors fails instead of continuing with a partially restored image.
- The complete loaded snapshot is decoded, canonicalized, and validated before any retained global is changed. If any retained entry is invalid, every retained global keeps the value it held before the load attempt.
- A save is encoded before it is published and is written through a temporary file in the destination directory. Failures before the atomic rename leave the previous file unchanged. A rename or parent-directory synchronization failure is reported as an error; the runtime does not claim that the new snapshot is durable. The in-memory snapshot remains dirty so a later save may retry.
- A due periodic save failure fails that scan before physical outputs are committed. A requested-stop save failure is exposed as the resource's retain error rather than reported as a successful stop.
- Cold restart does not load the retain store and therefore cannot silently reinterpret a failed warm-load attempt as cold-start success.
Retain codec compatibility and migration¶
The file-backed retain store preserves the logical snapshot across a successful
store/load round trip. The supported codec preserves exact names, order, and
the concrete values asserted by the compatibility profile: Boolean and integer
scalars, bounded arrays with their inclusive bounds, structures with their type
and field identity, and enumeration values with their declared type and
variant. This includes qualified retained PROGRAM variables. A warm restart
loads and applies the saved snapshot after rebuilding program instances; a
cold restart initializes retained variables and does not load the store. A
missing file alone represents an empty snapshot; a path that exists
but is not a readable retain file, or a parent path that cannot contain the
file, returns the underlying retain operation as an error instead of silently
substituting an empty snapshot.
The current checksum-protected format is version 2. Version-1 STRN snapshots
remain readable for the value tags supported by that format; a decoded value
materializes with the same logical name and runtime value. Successful
replacement publishes one complete snapshot and leaves no retain temporary
file. Parent-directory synchronization occurs after rename. If that final sync
fails, store returns an error and does not claim durability, but a read in the
same filesystem session may observe the already-renamed new snapshot. The
manager keeps the snapshot dirty and permits the next due save to retry without
another mutation notification.
Before applying a loaded snapshot, the runtime resolves every retained name and stages all compatible migrations:
- a safe declared numeric widening such as
INTtoDINTpreserves the mathematical value; - a retained
STRINGlonger than its current declared bound is canonicalized to that bound using the ordinary scalar-preserving string rule; - compatible enumeration values are rebuilt with the current declared enum type and variant identity, including the reviewed case-canonical spelling;
- compatible structures recursively rebuild the current declared structure and field identities, and compatible one-dimensional arrays preserve their inclusive bounds while recursively rebuilding each element;
- a structure field added by the current declaration receives its declared initializer or type default, while a field removed from the declaration is dropped; and
- a retained global absent from the current program is dropped as an orphan.
An incompatible nested structure field or array element rejects application; the runtime does not substitute a default or expose a partially rebuilt aggregate. These focused rules do not establish arbitrary-depth, multidimensional, renamed, removed-variant, or event-emission behavior.
Each successful value-shape migration emits RetainMigrationApplied; each
dropped orphan emits RetainOrphanDropped. These events identify the runtime
transition, not a stable debug-format string. Migration remains transactional:
all entries are validated and staged before any retained target changes, so one
incompatible entry rejects the whole snapshot without exposing earlier staged
values.
Power-loss guidance: retained values are only guaranteed to persist if the most recent
snapshot has been flushed to the retain store (i.e., at shutdown or after the save cadence).
Here, shutdown means the explicit requested-stop path. Plain destruction of a
Runtime or TestHarness is not a graceful-stop request and does not
implicitly publish dirty retained state. Unflushed changes may be lost on
sudden power loss (implementer-specific).
Direct restart preparation transaction¶
An in-process warm restart constructs its complete replacement storage and instance image before changing live state. If reviewed function-block instance construction fails, the restart returns an error and preserves the live executable, storage values, queued debug force, and cycle counter. This direct restart boundary does not independently specify task schedules, logical time, I/O bindings, process-image state, or a stable diagnostic string.
6.7.1 Online-change transaction¶
Online change is a truST runtime extension; IEC 61131-3 does not define its transport or transaction boundary. A reload request is applied only at a completed scan boundary. The runtime must prepare every fallible input needed by the change before replacing live execution state:
- decode and validate the complete bytecode container and materialize its VM module;
- validate the selected resource, tasks, and process-image sizes; and
- read, decode, canonicalize, and validate the retained snapshot required by the warm-reload policy; and
- construct the complete warm-restart variable and instance image in staged storage, including retained program variables and all fallible function-block, class, and program instance initialization.
When a caller names a bytecode resource, selection is case-insensitive but
must resolve that exact resource; an unknown name is an error and must not
silently select the primary resource. For compatibility with bytecode emitted
before resource identities were preserved, a module containing exactly one
resource named by the synthetic legacy placeholder RESOURCE may satisfy a
named selection. After that compatibility load, the requested name becomes the
runtime's active resource identity so subsequent bytecode encoding cannot
propagate the placeholder. The exception does not apply to multi-resource
modules.
If preparation fails, the request returns an error and leaves the prior
executable module, task schedule, variable storage, process image and bindings,
debug mutations, logical time, cycle counter, and runtime fault/status state
unchanged. The old program remains executable on the next scan.
For the reviewed retained INT(0..10) value 100, this rejection retains the
cause-identifying diagnostic text outside declared subrange 0..10.
The reviewed malformed byte sequence [00, 01, 02, 03] fails preparation as
the typed RuntimeError::Bytecode; no stable message text is implied.
After successful preparation, the commit replaces the executable and resource
metadata as one cycle-boundary operation, restarts at the program entry point,
applies the prepared retained snapshot, rebinds I/O, and returns the new runtime
metadata. Retained variables follow section 6.7; non-retained variables and
function-block instances follow warm-restart initialization. Logical time is
preserved, the scan counter restarts, and queued writes and forces are cleared
under section 6.9.2. A request must never report reloaded unless this complete
commit succeeds.
6.7.2 Runtime agent command conformance¶
The runtime implementation of the Agent API preserves the canonical workspace identity and validates the requested method and workspace-relative path before performing an operation. Its project, workspace, diagnostic, formatting, build, validate, test, runtime, and harness methods return the typed payload defined by the Agent API contract. Unknown methods and invalid paths return the stable reviewed error class instead of a false successful payload. Bounded test and harness loops must terminate at their requested case, iteration, or deadline boundary.
runtime.compile_reload completes diagnosis and compilation before requesting
the live reload. runtime.reload rebuilds the selected project before
replacing the live executable. A compilation, preparation, or reload failure
is returned as failure and leaves the current live runtime available; neither
method may report a successful reload until the complete replacement
transaction has committed. These runtime obligations specialize the general
Agent API method contract and do not widen its public method set.
6.8 Runtime Launcher & Deployment (Project Folder)¶
Production runtimes are started via the CLI (trust-runtime run) using a project folder
(runtime bundle format) directory. The launcher is responsible for:
- Loading the bytecode program (
program.stbc). - Loading runtime configuration (
runtime.toml). - Initializing I/O drivers (
io.tomlor system IO config). - Initializing retain storage (if configured).
- Exposing a control endpoint for local attach/debug.
- Validating bundle version compatibility before execution (internal
bundle.version).
The launcher must run on Linux, Windows, and macOS (desktop targets). Embedded targets may replace the launcher with platform-specific init systems while preserving the same configuration and control protocol.
If a project folder omits io.toml, the launcher loads the system IO config
This host-side launcher policy is a truST product contract. IEC 61131-3 does not define hardware-driver selection or OS-level I/O configuration, so the policy is not an IEC deviation.
Windows runtime executable stack link contract¶
The trust-runtime Cargo build script always declares itself as a rebuild
input. For a Windows target it assigns the trust-runtime executable an
8 MiB stack: MSVC targets emit /STACK:8388608, while other Windows toolchains
emit -Wl,--stack,8388608. Non-Windows targets emit no executable stack linker
argument. This is a build-platform contract for the runtime executable; it
does not change PLC task stack limits or establish runtime stack-usage bounds.
Runtime bundle builder source and dependency resolution¶
Without an explicit source override, the runtime bundle builder selects the
project's src/ directory. A project containing only the legacy sources/
directory rejects with the reviewed missing-src/ diagnostic rather than
silently changing source roots.
The reviewed local dependency graph resolves direct and transitive
trust-lsp.toml path dependencies, includes the root and dependency source
files in compilation and inspection, reports the reviewed LibA, LibB
dependency identities, and produces program.stbc after successful build.
For one unchanged reviewed local dependency graph, two consecutive builds
resolve the same dependency and source order and emit byte-identical
program.stbc content. This bounded repeatability contract does not establish
determinism for arbitrary registries, symlink graphs, concurrent filesystem
mutation, or cross-platform path representations.
check_program_stbc performs the same source discovery, dependency resolution,
compilation, and bytecode sizing as build, reports the reviewed root source and
dependency identity, and does not create or replace program.stbc.
Dependency resolution rejects a missing declared path, a cycle in the local dependency graph, and a requested package version that disagrees with the dependency manifest. The reviewed diagnostics respectively retain the missing dependency name, identify a cyclic dependency, and retain the requested version. Rejection returns no successful build report.
For the reviewed root project, a structure type declared in one source, a global of that type declared in a second source, and a program accessing the global structure field in a third source compile together and produce the bundle artifact while retaining all three source identities.
These focused builder rules do not prove arbitrary dependency ordering, symlink or concurrent-filesystem behavior, cleanup after partial writes, external registries, bytecode semantic equivalence, or cross-platform path behavior.
Namespace-qualified multifile runtime assembly¶
IEC 61131-3 Ed.3 Section 6.9 and Tables 64-66 define namespace membership,
qualified access, and the USING directive. The runtime source assembler
preserves those identities across the merged source set:
- a direct function member imported by
USINGis callable when the namespace and consuming program come from separate source units; - namespace-qualified programs are registered by their full name and remain executable scan entry points;
- sibling functions, function blocks, classes, and interfaces declared inside one namespace are registered by full name, resolve within that namespace, and execute as one semantic graph; and
- type and member defaults retain their declaring namespace/
USINGcontext, so a qualified or imported namespaced constant resolves identically across source order and from every consuming source; a missing or ambiguous import fails before runtime publication; and - program names are unique under ASCII-case-insensitive comparison across the merged source set. A collision rejects assembly, reports the duplicate condition, and retains the reviewed original spelling in the diagnostic.
These rules do not imply source-order dependence, arbitrary nested-namespace
import, complete diagnostic prose stability, or registration of declarations
that fail semantic validation. Namespace and USING semantics are IEC
behavior; runtime registry identities, scan-entry selection, and the bounded
collision diagnostic are truST integration behavior, not IEC deviations.
Runtime CLI entrypoint, diagnostics, and dispatch contract¶
The trust-runtime entrypoint parses one optional top-level command. A
successful parse dispatches that command exactly once to its corresponding
handler; omitting the command invokes the default runtime workflow. Handler
errors return to the entrypoint, which prints one error and exits non-zero
instead of continuing into another command path.
An invalid top-level command may produce one Did you mean hint before the
ordinary parser error. The hint selects the first registered command with the
smallest case-sensitive edit distance, only when that distance is at most two.
Global flag tokens are not treated as the mistyped command, surrounding
whitespace is ignored for comparison, and inputs outside the threshold receive
no potentially misleading suggestion.
For commands that accept a project folder, deprecated --bundle spellings
remain parser aliases for --project during the compatibility window. After a
successful parse, use of either --bundle <path> or --bundle=<path> emits one
deprecation warning before dispatch. It does not change project creation or
validation semantics.
run and play accept vm as the sole production
--execution-backend value. The retired interpreter value is rejected during
argument parsing, before handler dispatch, project access, or runtime mutation.
Ordinary handler failures exit with status 1. When a parsed command enables
--ci, the entrypoint uses the documented CI error classification, including
command-aware fallback codes. Known configuration and permission failures may
append one actionable tip to the original error text; the tip does not replace
or reclassify the underlying error.
For ide serve, the explicit project path, or the current directory when it is
omitted, must be an existing directory. A root runtime.toml is the primary
runtime; otherwise the lexicographically first immediate child containing
runtime.toml is selected. A selected runtime configuration must load
successfully before the web server binds. A workspace without any runtime
configuration uses the standalone config-ui identity so source-only
authoring remains available.
The standalone IDE source registry uses the selected runtime project's normal
recursive, deterministic .st and .pou build-source discovery, while
presenting paths relative to the selected workspace. Its synthetic control
state is local and nonexecuting: control authentication is disabled, debug
execution is not enabled, and discovery and mesh are disabled. The server
starts in standalone-IDE mode only after this state is constructed. These are
truST host-product rules outside IEC 61131-3 and are neither an IEC decision
nor an IEC deviation.
The deprecated config-ui serve alias emits its deprecation warning before
entering this same standalone-IDE validation and startup path; it does not own
a second project-selection or server mode.
Runtime conformance case runner¶
trust-runtime conformance consumes an explicit suite directory or the
conformance directory below the current working directory. The suite root
must exist as a directory. Case discovery is deterministic and fail-closed:
only the registered category directories are accepted; every case directory
requires manifest.toml; the manifest ID and category must match their
directory names; and IDs use
cfm_<category>_<nonempty-lowercase-token-sequence>_<three-digits>.
Manifest source paths are relative, contain no parent/root/prefix component,
and must resolve inside their owning case directory.
The runner supports three closed case kinds:
- A
runtimecase requirescycles > 0. Every nonempty advance, named-input, and direct-input series has exactly one element per cycle. Restart directives target1..=cycles;warm,hot, andfaultuse warm retained-state behavior, whilecoldanddownloaduse cold behavior.skipand_preserve the previous step value. Typed inputs useKIND:VALUEfor BOOL, signed and unsigned integers, bit strings, REAL/LREAL, TIME/LTIME, and STRING, with the declared runtime width enforced. - A
compile_errorcase succeeds only when compilation fails. Successful compilation is a case-execution error and must never be blessed as expected failure output. - A
connector_status_tracecase requires at least one step. Its closed source vocabulary is ADS connection/status, OPC UA client/server, MQTT session, Modbus, EtherCAT, and generic I/O-driver health. Source and state tokens are trimmed, ASCII case-insensitive, and treat hyphens as underscores. Unknown tokens and expected state/health mismatches fail the case.
Cases execute in ascending case-ID order. Runtime artifacts contain the
declared observations for every cycle; connector artifacts contain every
projected step. --update-expected writes canonical pretty JSON to the
category's expected-artifact path. Verify mode distinguishes a missing or
unreadable expectation from a readable value mismatch, writes actual mismatch
artifacts, and emits stable reason codes. Suites containing only the six legacy
categories use summary profile/version trust-conformance-v1/1; any expanded
category selects trust-conformance-v2/2. The command writes and prints the
same summary and returns an error whenever any case failed or errored. These
runner and artifact rules are truST product behavior outside IEC 61131-3 and
are neither an IEC decision nor an IEC deviation.
Runtime launcher input, validation, and startup contract¶
trust-runtime play --project <path> is the product convenience entry point.
It creates or completes the selected project when the folder is missing or
lacks runtime.toml or program.stbc; project I/O may instead come from the
system configuration. An existing non-directory project path is rejected
without mutation.
trust-runtime play without --project is the first-run creation exception to
the general optional-project-resolution contract. It first performs standard
bundle detection. A successful detection selects that project and applies the
same completeness rule above. Only the ordinary no-project-found outcome
creates the default project in the current directory. Current-directory access
failures and system-I/O configuration read, parse, or validation failures are
preserved and abort before project mutation.
All non-filesystem launch arguments, including restart mode and simulation time
scale, must be validated before project detection, creation, or any other
project mutation. A rejected option therefore leaves a previously missing
project path missing. trust-runtime run consumes an existing project and does
not provide this creation behavior.
Project-mode startup recursively compiles the .st and .pou sources beneath
the project's selected source root, case-insensitively by extension, together
with sources from resolved local project dependencies. Legacy --config
startup recursively compiles those source types beneath its literal runtime
root. Source discovery must not interpret project or directory names as glob
syntax, must use deterministic path ordering, and must propagate directory and
file-read failures. An empty or invalid source set is a startup error. Startup
without a project or legacy configuration is the IDE-shell mode: it compiles
one synthetic empty Main program rooted at __ide_bootstrap__.st, exposes no
bundle, and marks the loaded runtime as an IDE shell instead of claiming that a
project was loaded.
Before the first PLC scan, the launcher must successfully complete all startup preparation: bundle-version validation; source and bytecode validation; runtime, watchdog, fault, safe-state, telemetry, I/O-driver, and retain configuration; configured ADS and OPC UA client startup; execution-backend selection; control endpoint and TLS validation; and enabled control, protocol, discovery, mesh, historian, persistence, and web-service initialization. Failure in any of these steps aborts startup; the scan start gate is not opened and the launcher must not report the PLC as running. Warm startup loads and validates the configured retain snapshot before execution. Cold startup does not read retained values. Bundle-version compatibility is checked before watchdog, fault, or other runtime policy is mutated; an unsupported version leaves the prior runtime policy unchanged. A disabled ADS or OPC UA client is a startup no-op and must not construct a transport. Enabling either client without its loaded sidecar configuration is a startup error detected before transport construction or connection-state mutation.
Simulation startup rejects a CLI or project time scale of zero. --simulation
enables simulation without discarding the project's simulation model; a CLI
time scale greater than one enables simulation and overrides the project time
scale. Otherwise the loaded project simulation configuration remains
authoritative. The settings exposed to control and status surfaces must reflect
the effective runtime configuration and this effective simulation/backend
selection, rather than an independent set of launcher defaults.
A simulated launch exposes a warning that names simulation mode, the effective
time scale, and that the mode is not for live hardware. Production mode emits
no simulation warning.
trust-runtime validate --ci validates the same bundle version, TLS, control
authentication, enabled I/O-driver configurations, bytecode container, module,
and selected-resource requirements used by startup. Its versioned JSON success
payload reports only enabled I/O drivers in io_driver and io_drivers, in
configuration order; disabled configuration entries are not reported as active
runtime drivers.
Runtime structured logging accepts error, warn (warning), info,
debug, and trace, case-insensitively. Empty or unknown configured levels are
configuration errors, not aliases for info. Every emitted line is one JSON
object containing a Unix-epoch millisecond timestamp, canonical level, event
name, and event data; events below the configured verbosity are omitted.
SIGINT and SIGTERM request the ordinary bounded runtime stop path so shutdown
and safe-state behavior completes before the launcher emits runtime_exit and
returns success.
Operator-facing I/O provenance labels preserve the selected driver direction.
For Modbus, an input address uses the configured input-register base and an
output address uses the configured output-register base, with the addressed
word offset added to that base. For MQTT, an input address names topic_in and
an output address names topic_out; the opposite-direction topic is never
substituted.
6.8.0 Deprecated workbench command forwarding¶
During the documented compatibility window, deprecated trust-runtime
workbench aliases forward their complete argument vector to trust-dev after
printing the removal-not-before warning. Binary resolution uses an explicit
TRUST_DEV_BIN value first, then an executable regular file named trust-dev
beside the running trust-runtime binary, then normal PATH lookup. A directory
or non-executable sibling must not shadow a usable PATH command.
The forwarding process preserves a normal child exit code. On Unix, when the
child terminates from signal n, the alias exits with the conventional shell
status 128 + n instead of flattening the failure to generic status 1. Spawn
failures remain normal CLI errors with installation guidance.
6.8.1 Scriptable control client¶
trust-runtime ctl exchanges one newline-delimited JSON request and response
with the selected runtime control endpoint. --endpoint overrides the endpoint
from the project runtime configuration. Credential resolution is independent
of endpoint resolution and uses this precedence: explicit --token, then
TRUST_CTL_TOKEN, then runtime.control.auth_token from --project. The
project token remains eligible when --endpoint overrides only the endpoint;
an explicit or environment token does not require loading the project merely
to resolve credentials.
Each CLI action maps to one control request with numeric id = 1, its
registered control operation name in type, the resolved token in auth, and
only the parameters declared by that action. The client accepts exactly one
JSON response line. An empty or malformed response is a transport/protocol
failure. The response must end with a newline and contain the same non-negative
integer id as its request; a missing, noninteger, or mismatched response ID is
a protocol failure. A response with ok = false, or without a Boolean ok, is
a command failure and must produce a non-zero process exit rather than being
printed as a successful scripted result. A TCP connection attempt is bounded to
500 milliseconds;
after a TCP or Unix connection is established, reads and writes are bounded to
750 milliseconds, and a response line may not exceed 1 MiB.
config.set values trim surrounding whitespace and decode case-insensitive
null, true, and false, followed by an in-range signed 64-bit decimal
integer; every other value remains the trimmed string. Human-oriented rendering
happens only after ok = true. Status renders
state=<state> fault=<fault> rt_profile=<profile> rt_active=<bool>, defaulting
an absent or JSON-null fault to none, and absent realtime fields to
disabled and false. A non-null fault must be a string. Health renders
ok=<bool>. An empty task list renders tasks=0; otherwise each task renders
one line containing its name, min/average/max/last milliseconds to three
decimal places, and overrun count.
For status, health, and task summaries, a missing or wrong-typed required result
field is a command failure; it is not rendered as raw JSON or replaced with an
invented value.
Runtime control request, job, and state-surface contract¶
Each newline-delimited control request is decoded exactly once. Malformed JSON
returns a negative response without dispatch or mutation. A request type absent
from the registered operation set returns the stable error
unsupported request; every registered type routes to its owning handler.
Missing or malformed required parameters return a parameter error rather than
unsupported request. A routing behavior lock proves only that the registered
handler boundary was reached; it is not evidence that every operation
succeeded or returned its complete semantic payload.
Asynchronous operations return a nonempty job ID. Job status is running,
complete, or failed; an unknown ID returns a stable machine-readable
not-found error. Terminal success retains its report and terminal failure
retains its reason. Work that cannot run because a required build feature is
absent terminates as failed rather than remaining indefinitely running. ADS
client and server Doctor use this same job contract. Synchronous and live ADS
operations that require ads-wire fail explicitly when it is unavailable and
must not synthesize discovery, Doctor, route, or import results.
The status and configuration surfaces project one effective runtime state:
status.execution_backend,status.execution_backend_source,status.metrics.execution_backend, and the correspondingconfig.getvalues agree. Omission selects backendvmwith sourcedefault.- Status preserves resource identity, control and simulation mode, cycle and
profiling metrics, realtime requested and observed state, warnings, errors,
and every I/O-driver health entry. The default realtime projection is
disabled with scheduler
otherin both status andconfig.get. - Aggregate health is false when any I/O driver is faulted and retains the faulted driver's stable error detail; runtime readiness alone cannot hide that fault.
config.setuses a closed key, type, and value contract, validates cross-field security requirements, and rejects invalid input atomically.runtime.execution_backendis startup-only and cannot be switched through live control. This atomicity is validation-before-logical-mutation; crash- atomic filesystem replacement is separate verification depth.historian.queryandhistorian.alertsreturn anitemsarray, honor the requested limit, and return matching persisted samples or alerts when they exist.
Status, health, and task-stat projection is fail-closed and deterministic.
status returns an error when runtime settings, control mode, I/O health,
metrics, or realtime-status state is unavailable; it must not substitute
production mode, VM defaults, an empty driver list, zero metrics, or a default
realtime posture for an internal read failure. tasks.stats likewise returns
an error when metrics are unavailable. health returns an error when I/O
health or realtime status is unavailable.
The aggregate health.ok value is true only when the resource is running,
ready, or paused, has no resource fault, has no faulted I/O driver, and has no
realtime error. A degraded I/O driver or realtime warning remains visible but
does not alone make the aggregate false. Every I/O entry contains its exact
name and one of ok, degraded, or faulted; only degraded and faulted
entries contain their stable error detail. Status preserves I/O registration
order. Task statistics are sorted lexically by task name, independent of map
iteration order. Profiling contributors retain contribution-descending order
with their established lexical tie-break.
events.tail and faults accept either no parameters or a strict object
containing only optional limit. The default is 50 and the accepted range is
1 through 1,000 inclusive. Null, a non-object, an unknown key, a non-integer,
a Boolean, or an out-of-range value rejects instead of silently selecting a
default. Both operations return newest entries first and fail when the event
store is unavailable. events.tail applies the limit to all events. faults
first selects only RuntimeEvent::Fault entries and then applies the limit, so
intervening non-fault events cannot reduce the requested number of faults.
SafeStateFailed remains its own event type and is not silently relabeled as
the originating resource fault.
Historian query parameters are strict objects. historian.query permits only
variable, since_ms, and limit; historian.alerts permits only limit.
An omitted object uses limits 250 and 200 respectively. Query limit is in the
range 1 through 5,000 inclusive and alert limit is in the range 1 through
1,000 inclusive. since_ms is an exactly representable nonnegative integer.
When present, variable is a string that is non-empty after trimming; the
trimmed exact path is used for matching. Unknown fields, null, wrong types,
fractional values, empty variable names, and out-of-range limits reject with
invalid params rather than relying on the historian's defensive internal
clamp. A disabled historian still returns historian disabled and does not
claim that an invalid query was executed.
config.set helper validation is exact and side-effect-free. Boolean fields
accept only JSON booleans. Positive-integer fields accept an exactly
representable signed 64-bit JSON integer in the range 1 through i64::MAX;
zero, negative, fractional, Boolean, string, container, and larger unsigned
values reject. Non-empty strings trim Unicode whitespace and reject all-empty
input.
String arrays preserve order, trim every entry, and reject a non-array, non-string entry, or empty trimmed entry. String maps require an object, trim both keys and values, reject empty trimmed keys or values and non-string values, and emit deterministic key order. Empty arrays and objects are valid where the owning setting permits an empty collection.
Runtime-cloud WAN rules are a strict array of objects containing exactly
action and target; link-preference rules contain exactly source, target,
and transport. Missing, null, wrong-typed, empty, or unknown fields reject
with the owning key and zero-based entry index. All strings are trimmed.
Transport uses the closed runtime-cloud vocabulary and canonicalizes the
Modbus hyphen/underscore alias. Rule order is preserved.
Validation errors have the stable prefix
invalid config value for '<key>':. They may identify an entry or unknown
field but do not serialize sibling values. Helper rejection performs no state
mutation and cannot append an item to either the updated or restart-required
result.
Live config.set requires an object; an absent payload and a non-object payload
are distinct errors. An empty object is a successful no-op with empty
updated and restart_required arrays. The handler stages the complete
effective settings, control token, control mode, and debug-enabled state,
validates every key and all final-state cross-field requirements, and only then
commits any of them. An unknown key, startup-only key, type/value error,
cross-field error, unavailable lock, or scheduler-update rejection leaves all
four state domains unchanged.
The final state may use web.auth = token only with a non-empty final
control.auth_token. This applies both when token mode is selected and when an
existing token is cleared. A TCP control endpoint that requires authentication
never accepts a null token. Supplying a new token and selecting token web auth
in one object is order-independent. Token and mesh-token values are trimmed,
never returned, and never included in success or error text.
updated contains each accepted request key exactly once in canonical lexical
order. restart_required is the lexical subset whose effective runtime service
cannot change in place: retain mode; web, discovery, mesh, runtime-cloud, and
control-mode settings. Log level, watchdog settings, fault policy, retain save
interval, control token, and debug-enabled state are live updates and do not
claim restart. A rejected request returns neither list.
config.get returns the complete effective key projection without credential
values. Control and mesh credentials expose only presence; control additionally
exposes length when present and null length when absent. Enum-like values use
the same canonical lowercase or snake-case spelling accepted by configuration:
watchdog actions, fault policy, retain mode, control mode, mesh role,
runtime-cloud profile/transport, realtime scheduler, and execution backend.
Derived startup/backend-source, simulation, observability, and credential
presence fields are read-only observations rather than implicit authorization
for config.set.
Runtime execution-backend selection and construction contract¶
The host-runtime execution-backend selector trims surrounding whitespace and
matches ASCII case-insensitively. The only accepted value is vm, which selects
BytecodeVm. Empty or unknown values reject with an invalid-configuration
error that names runtime.execution_backend and preserves the rejected text.
The retired interpreter value rejects with the distinct migration guidance
that it is no longer supported for production runtimes and that vm must be
used.
A freshly constructed Runtime selects BytecodeVm, has zero elapsed logical
time and zero completed cycles, and exposes the default date/time profile with
epoch tick zero and one-millisecond resolution. Selecting BytecodeVm is valid
before a bytecode module has been loaded. For a runtime built from accepted
source, the first VM cycle may materialize the module lazily; that cycle must
complete successfully and expose the source program's resulting state.
When a metrics sink is attached, the snapshot after the reviewed VM cycle
reports BytecodeVm as the effective execution backend.
This contract does not authorize another production backend, cross-version bytecode compatibility, arbitrary source semantics, optimized-backend parity, or a general performance claim.
Runtime historian recording and export contract¶
An enabled historian in all mode records the reviewed global BOOL, integer,
floating-point, and string values with their runtime types. An accepted capture
at time t establishes the sample-interval boundary: a second request before
t + sample_interval_ms returns zero and adds no sample, while a request at or
after that boundary records the current snapshot. Queries for the reviewed
paths return the recorded values in capture order.
In allowlist mode, exact configured names and the reviewed retain.* prefix
are recorded; unmatched globals are absent. A file-backed historian reloads
the reviewed persisted sample when a new service instance opens the same
history path. The Prometheus renderer includes the reviewed runtime uptime and
fault totals together with historian sample and alert totals when the historian
snapshot is supplied.
For the reviewed single-variable above alert, two consecutive samples above
the threshold satisfy debounce_samples = 2 and emit one Triggered event.
The next below-threshold sample emits one Cleared event. A configured file
hook receives exactly one line for each emitted transition, for two lines in
the reviewed trigger/clear sequence.
These cases do not establish arbitrary value conversion, wildcard grammar
beyond the reviewed exact and retain.* forms, concurrent writers, crash
durability, retention limits, arbitrary alert expressions or hook transports,
or the complete Prometheus schema.
Runtime metrics window and profiling contract¶
Runtime profiling starts enabled. While enabled, a recorded call is grouped by
its kind:name identity, increments its call count, and contributes its elapsed
time to the per-cycle average. A profiling snapshot retains the reviewed call
identities and ranks the reviewed program call ahead of the smaller
function-block call by average cycle contribution.
Disabling profiling clears accumulated call entries and top contributors and ignores calls recorded while disabled. Re-enabling profiling starts a clean collection, and a subsequently recorded call appears as the sole reviewed entry. For the reviewed non-wrapped cycle window containing 5, 10, 15, 20, and 25 milliseconds, the snapshot reports five samples, a median of at least 10 milliseconds, a p95 not below the median, and a maximum of 25 milliseconds.
Cycle, task, and call durations are represented in milliseconds as finite
f64 values derived from non-negative monotonic durations. The first sample
sets minimum, maximum, average, and last to the same value. Later samples update
minimum and maximum, set last to the newest value, and compute the arithmetic
mean over every accepted sample. An unrecorded statistics object exposes zero
for every duration field and zero samples/calls. Recording a zero duration is a
real sample, not an absent value.
Cycle percentile reporting uses a rolling window of the most recent 512 cycle
durations. The 513th sample evicts only the oldest sample. Snapshots sort a copy
of the retained values and do not mutate collection order or future rollover.
For a sorted population of n, p50, p95, and p99 use index
round((n - 1) * q) for quantile q; quantiles below zero clamp to zero and
above one clamp to one. An empty window reports zero percentiles, zero maximum,
and zero samples. Every non-empty snapshot reports the exact retained sample
count, p50 <= p95 <= p99 <= maximum, and finite values.
Task identities are exact and independent. A recorded task duration creates or
updates only that task. A positive missed-activation count increments both the
runtime-wide overrun counter and that task's overrun counter with saturating
u64 arithmetic. A zero missed count is a no-op and does not materialize an
otherwise absent task. Fault counts also saturate. Snapshot task rows are
sorted by task name so identical runtime state has deterministic JSON and
control-surface projection regardless of hash-map insertion order.
Call identities are the exact kind:name pair. Each call row reports its own
minimum, maximum, mean, last duration, call count, and total call time divided
by the number of recorded cycles as avgCycleMs. When no cycle has been
recorded, the denominator is one so a recorded call remains visible, while its
cycle percentages are zero because no cycle budget exists. Multiple calls with
the same identity aggregate; different identities never merge.
Top contributors contain at most five call identities, ordered by descending
avgCycleMs and then ascending identity key for ties. cyclePct is
avgCycleMs / cycle.avgMs * 100; lastCyclePct is
call.lastMs / cycle.lastMs * 100. A zero cycle denominator yields zero rather
than NaN or infinity. Percentages are not clamped to 100 because nested or
overlapping instrumentation may legitimately sum beyond one cycle budget.
Setting profiling from enabled to disabled clears all accumulated call rows and contributors. Calls while disabled are ignored. Repeatedly setting the already disabled state remains empty; repeatedly setting the already enabled state preserves current samples. Re-enabling after a disabled interval begins with an empty profile and does not reconstruct ignored calls. Cycle, task, fault, and overrun metrics are independent of the profiling toggle.
A metrics snapshot is observational: it does not reset counters, consume samples, reorder retained window state, or alter profiling enablement. Successive snapshots without new records preserve all metric values except that monotonic uptime may increase. The default and explicitly selected execution backend are reported exactly.
These rules establish the in-memory accounting and snapshot contract. They do not claim wall-clock accuracy, lock-free concurrent collection, instrumentation completeness, a stable observer-overhead budget, or a general execution-time guarantee.
Runtime terminal console contract¶
The optional trust-runtime ui terminal console is an operator-facing
projection of the versioned control protocol. It does not own a second runtime
state model and it does not make a successful control operation out of a
failed, malformed, or unavailable control response. These are truST product
rules outside IEC 61131-3.
Endpoint selection is deterministic. An explicit endpoint is parsed and used
without requiring a bundle. Otherwise the selected bundle supplies the
endpoint and optional configured authentication token. An explicit token has
precedence over the environment and bundle; a nonempty environment token has
precedence over the bundle. Empty token text is absence, not a credential.
Every request is one newline-delimited JSON object and carries the selected
token in auth. The refresh transaction requests, in order, status,
tasks.stats, io.list, events.tail with limit 20, and config.get, using
distinct request IDs 1 through 5. Transport or decoding failure aborts that
refresh; the console retains the last complete snapshot instead of installing
a partial one.
Response projection has closed fallback behavior:
- status requires an object result and a string state; absent optional
counters and timings are zero, the resource is
resource, the fault isnone, the control mode isunknown, and simulation defaults to production at scale one; - task, I/O, and event projections preserve response order and return an empty
list when
resultis absent or not an array; - I/O string values remain unquoted while non-string JSON scalars use their JSON spelling;
- event levels
faultanderrormap to Fault,warnandwarningmap to Warn, and other or absent levels map to Info; - settings require a result object and use the documented runtime defaults. Negative or unrepresentable cycle intervals and simulation scales are rejected as absent and defaulted rather than narrowed.
Console configuration is advisory. An absent, unreadable, or malformed
runtime.toml, an absent console table, and a layout containing no recognized
panel names all select the built-in layout. Recognized layout names are
case-insensitive, retain declared order, and discard unknown entries. A
positive console.refresh_ms overrides the CLI default only when the caller
left that default unchanged; zero and negative refresh values are invalid and
do not create a busy loop.
Prompt editing is UTF-8 safe: the cursor always denotes a character boundary, left/right/backspace operate on one complete Unicode scalar, and no accepted key sequence may panic. Activation positions the cursor at the end of the initial text; deactivation clears transient history navigation. Empty and whitespace-only commands are not retained. History traversal saturates at the oldest entry, advances toward newer entries, and clears the input after the newest entry. Suggestion traversal wraps in both directions, and selecting a suggestion records and executes that exact slash-prefixed command.
Read-only mode accepts only observation and exit interaction. / reports
read-only state without activating a prompt, and q exits. It must not issue
pause, resume, stepping, restart, shutdown, I/O, configuration, or other
mutating requests. Beginner mode exposes only its documented beginner command
set and disables direct debug controls. A confirmation performs an action only
for case-insensitive y or yes; every other response cancels. Escape and
control-C leave a normal inactive prompt without executing the pending
operation.
The cycle sparkline retains at most 120 finite nonnegative samples, rounded to
tenths of a millisecond with zero rendered as the minimum visible bucket.
Absent, NaN, infinite, or negative cycle values do not become fabricated large
samples. Alerts retain at most five newest entries. Event alert deduplication
uses the complete event identity (code, timestamp, level, and message), so a
new occurrence reusing a code is not hidden; informational events do not
produce alerts. Watch evaluation preserves watch-list order and classifies a
missing result as unknown, a protocol error as error: <message>, and a
transport failure as unavailable.
Command and menu selection is deterministic. Command names are the documented
lowercase tokens, optional leading / is accepted, numeric menus are
one-based, and invalid or out-of-range choices retain the current mode with an
explicit error. Navigation wraps over the currently eligible entries. I/O
Read may select any listed point; Set, Force, and Unforce offer output points
only. Boolean values receive the TRUE/FALSE chooser, while other values enter
the explicit value prompt. A local force marker is added or removed only after
the corresponding control response succeeds. unforce all retains markers
whose release failed and reports the failure; it must not claim that all
forces were released after a partial failure.
Settings distinguish live acceptance from durable project mutation. Boolean inputs accept the documented true/false aliases; TOML scalar parsing trims text and preserves Boolean, signed integer, or string type. Nested TOML update creates missing tables but rejects traversing a scalar. A project-backed setting is reported saved only after the candidate TOML validates and the file write succeeds. A control-backed setting is reported saved only after the control response is successful; when both live and durable updates are required, either failure is visible and success is not fabricated. Restart menus open only for an accepted change that actually requires restart.
These unit-level rules prove console parsing and state transitions. They do not replace an interactive terminal smoke test for rendering, terminal restoration, or a live runtime control connection.
The role and transport rules in section 6.9.4 govern these handlers. A pairing token retains its stored role; an unauthenticated network connection receives Viewer authority; and a local mode-0600 Unix socket without configured authentication may receive Admin authority. Sufficient role permits a request to reach its handler but does not override a configured HMI read-only posture.
Runtime HMI descriptor, live-state, and control contract¶
The runtime HMI schema is the authority for every exposed widget and process binding. A schema row has a stable widget ID and path, declared type, current quality, writable flag, and any configured display, unit, range, alarm, or layout metadata. Descriptor overrides may change presentation without changing the stable identity of an unchanged binding. Unknown paths, unknown widgets, type mismatches, and invalid limits are reported as binding errors; they are not silently dropped or coerced.
HMI values are read directly from the runtime snapshot and preserve the declared runtime type, timestamp, and quality. The in-process read and write ports use the same typed contract as the JSON control surface but do not serialize and parse an intermediate JSON request. A write is admitted only when all of the following are true:
- HMI writes are enabled and the runtime is not in HMI read-only mode;
- the target resolves by stable widget ID, canonical path, or configured alias;
- the resolved target is in the explicit write allowlist and the schema marks it writable; and
- the value passes the same declared-type, subrange, bounded-string, and finite floating-point checks as other runtime writes.
An admitted write is queued for the cycle boundary and audited through the HMI policy path. A rejected write queues nothing and leaves runtime storage unchanged. The queue-processing work remains bounded by the configured cycle budget.
Descriptor updates are atomic at the schema boundary. A valid descriptor or scaffold reset increments the schema revision and becomes visible without a runtime restart. Rapid successive file changes must not deadlock the watcher. An unreadable or invalid descriptor retains the last good schema and exposes the validation failure instead of publishing a partial schema. When descriptor files are absent, the runtime may publish the deterministic inferred layout.
The live event stream emits changed values only, includes stable widget IDs and the active schema revision, and deduplicates identical alarm payloads. Trend downsampling preserves the requested time window and its first, last, minimum, and maximum bounds. Alarm state uses the configured alarm label and follows the ordered lifecycle raise, acknowledge, clear, and history retention. A configured deadband requires the value to re-enter the clear window before the alarm clears. When HMI persistence is configured, restart restores only the bounded trend window and alarm history owned by that configuration; it does not expand the configured per-series limit. HMI control failures use a structured error payload with a stable code; clients do not need to parse diagnostic prose.
Runtime control audit and debug projection¶
Every accepted or denied control operation attempts to emit its audit record.
If the audit sink is unavailable, the request retains its normal semantic
result and audit identity, and the runtime emits an AuditDropped event; it
must not pretend durable audit succeeded. A request for a compiled-out or
disabled feature returns the stable code feature_disabled and emits a
FeatureDisabled event.
Debug variables and evaluation expose the declared public type name and value, not internal instance IDs. A stale frame, unknown variable reference, unknown evaluation name, or unavailable/poisoned I/O snapshot fails closed with a negative response. These failures do not return an empty successful projection or mutate debug or runtime state.
Runtime control I/O and variable mutation contract¶
The attach-mode JSON control surface uses strict parameter objects for debug
mutation. io.write and io.force require exactly string fields address and
value; io.unforce requires exactly string field address. eval requires
exactly string field expr; set requires exactly string fields target and
value; var.force requires exactly string fields target and value; and
var.unforce requires exactly string field target. Missing parameters use
the stable missing params failure. Null, a non-object, a missing field, a
wrong-typed field, or an unknown field rejects as invalid params. Rejection
queues no write and changes no force.
Control I/O addresses use the canonical uppercase %I, %Q, or %M grammar
after trimming surrounding whitespace. A mutation address is concrete:
wildcards reject. Flat addresses must fit the 16,777,216-byte area, including
the complete width of byte, word, double-word, and long-word accesses.
Hierarchical addresses retain every declared path segment and bit coordinate.
The same address eligibility applies to write, force, and release; release
cannot use an address that write or force would reject merely because no new
value is supplied.
io.list returns the cached snapshot directly and fails with
no snapshot available before the first captured scan. io.read returns
{"snapshot": null} for that legitimate pre-scan state. If the snapshot lock
is unavailable, both operations fail with I/O snapshot unavailable; they do
not collapse an internal failure into a pre-scan state. A returned snapshot is
one coherent cached scan, including only the force marks captured with that
scan.
The attach-mode scalar text grammar is deliberately small. After trimming,
TRUE and FALSE match ASCII-case-insensitively and produce Boolean values.
A base-ten signed integer accepted exactly by the i64 domain produces a
LINT; optional leading sign and leading zeroes are accepted, while an empty
string, separators, base prefixes, decimal points, exponent notation, quoted
text, and out-of-range integers reject as unsupported value. This admission
grammar does not bypass target-width or declared-type checks when the queued
mutation is applied at a scan boundary.
An accepted io.write appends one one-shot request in arrival order. Multiple
writes to the same address remain distinct ordered requests. An accepted
io.force creates an active force; forcing the same exact address again
replaces its value in place without duplicating or reordering the force.
io.unforce removes only the exact matching force and is idempotent when no
such force exists.
The bounded eval operation trims expr, rejects an empty name, and performs
an exact snapshot lookup: global storage first, then retained storage. It is a
name read, not general expression execution. An absent snapshot returns
no snapshot available; an unknown name returns unknown identifier.
Successful values use the stable runtime debug representation.
Variable mutation targets use exact lowercase prefixes. set admits
global:<name> and retain:<name> only. Force and release additionally admit
instance:<id>:<name>, where id consists only of ASCII decimal digits in
the inclusive u32 range. Surrounding whitespace on the complete target is
not ignored because it changes the prefix; whitespace around the name after
the delimiter is trimmed. Every name must remain non-empty. Instance names
retain all text after the second delimiter, after surrounding whitespace is
removed.
An accepted set queues a one-shot variable write for the next scan. A later
pending write to the same exact target replaces the earlier value in place;
different targets retain arrival order. Force uses the same target identity
and scalar grammar. var.forced returns active variable forces in stable
insertion order with canonical targets global:<name>, retain:<name>, or
instance:<normalized-u32>:<name> and stable debugger-formatted values.
Re-forcing one target replaces its value without duplication. Release removes
only the exact target and is idempotent.
6.8.1a Offline communication authoring client¶
trust-runtime comm is a scriptable local authoring surface. Each accepted
subcommand delegates to exactly one matching offline communication operation
and emits that operation's JSON result:
schemaoptionally limits the schema request to the selected protocol;topologyreads the selected project and reports its offline fleet topology;applyrequires--paramsto decode as a JSON object, forwards the selected protocol and action, and includesinstance_idonly when one was supplied;discoverforwards the selected protocol, origin, passive flag, and only the optional scope fields that were supplied by the caller;browse-symbolsaccepts an optional JSON-object target, optional cached JSON snapshot, instance, and connection name, and selects project-aware browsing only when a project was supplied;opcua-trust listreports the selected client PKI directory and its trusted server certificates, whileopcua-trust clearremoves those trusted certificates and reports the number removed.
comm discover defaults to passive read-only discovery when --passive is
omitted and preserves omitted host, adapter, and other optional scope fields as
absent. comm browse-symbols defaults kind to symbols and preserves omitted
project, snapshot, and instance inputs as absent.
Malformed JSON, a non-object --params or target, an unreadable or malformed
snapshot file, and any delegated operation failure are command failures. They
must not be printed as successful communication results. These offline CLI
rules are truST host-product behavior outside IEC 61131-3 and are neither an
IEC decision nor an IEC deviation.
The default runtime template consumed by offline topology and apply operations must itself satisfy current runtime configuration validation. An unset optional mesh authentication token is omitted; the template must not serialize an empty secret value that the parser rejects even while mesh is disabled.
Communication schema, capabilities, and Test¶
Communication schema and capability responses use schema version 4. Protocol filtering returns only the requested protocol. Every schema entry has a stable protocol ID, category, apply mode, configuration owner, multi-instance capability, and typed field definitions. Secret fields are marked as secret metadata, have no secret default, and never serialize a credential value.
The schema registry is a closed, deterministic product contract. Its protocol
order is modbus_tcp, mqtt, ethercat, gpio, simulated, loopback,
opcua, opcua_client, openot, discovery, mesh, realtime_t0,
runtime_cloud, ads_server, and ads. Protocol IDs and action IDs are
unique. Filtering trims surrounding whitespace, folds ASCII case, and treats
hyphens as underscores; an unknown string selects no protocols, while a
non-string protocol filter is malformed and must not silently return the
unfiltered registry.
Each protocol has non-empty title, purpose, availability, primary category,
configuration home, apply mode, lifecycle effect, and at least one category
containing its primary category. The closed configuration homes are
io.toml, runtime.toml, opcua_client.toml, and ads.toml; every protocol
must route to the apply implementation that owns that home. The closed action
vocabulary is add, edit, upsert, remove, disable, discover,
browse_symbols, test, doctor, and route_script. Actions are unique per
protocol. supports_test is true exactly when test is advertised; a schema
must not expose one half of a usable Test control.
Field IDs are non-empty and unique within a protocol. Labels and help are
non-empty. The closed field-type vocabulary is string, secret, boolean,
number, enum, endpoint, path, json_array, and json_object, and a
default has the matching JSON shape. Required fields have a non-null default.
Only secret fields carry secret = true; their default is always null and
they are never required. Enum options are non-empty, unique strings containing
the default. Number validation uses a complete integer range with min <= max
and a default within that range. Endpoint validation is either host_port or
socket_addr. A visibility predicate references another field in the same
protocol and its comparison value is valid for that controlling field.
Schema defaults and apply validation are one contract, not separate sources of
truth. Every directly applicable default set is accepted by its owning
validator. Every advertised integer bound, enum option set, endpoint shape,
required string, array shape, and object shape is enforced by that same apply
route. ADS-client and OPC-UA-client connection arrays are intentionally empty
authoring seeds: while enabled is true, they are incomplete until the user
selects at least one connection and point/node, and the apply route must reject
activation without that selection.
Configured io.toml instances retain stable protocol-qualified IDs and appear
only under their matching protocol. Instance parameters are an observation
projection, not a credential retrieval surface: secret-valued keys such as
passwords, tokens, and private keys are omitted or replaced with a non-secret
presence marker before serialization. Schema filtering and configured-instance
projection must never cause a persisted credential to appear in a response,
diagnostic, or display name.
The version-4 capability registry contains exactly the 15 registered communication protocols. Build availability, platform availability, configuration state, and operational evidence are distinct. Every protocol shown by default on the current platform must have a built backend; an unknown platform marker fails closed instead of silently removing that requirement.
A communication Test request may succeed at the control-envelope level while
returning result.ok = false for a completed negative protocol test.
Unsupported Test operations return supported = false and must not appear as
usable controls.
comm.test requires a non-empty string protocol and an object-valued params;
an absent request or protocol fails at the control-envelope boundary, while a
present non-object params value returns a structured params field error
before protocol work rather than being reinterpreted as an empty target.
Protocol normalization follows the communication-schema rules. The supported set is exactly Modbus TCP,
MQTT, OPC UA client, simulated I/O, and loopback I/O. Simulated and loopback
return successful local_driver evidence without opening a socket. EtherCAT
and GPIO return a stable hardware-health explanation with supported = false.
Every other registered or unknown protocol returns the stable unsupported
result without network activity, evidence, or field errors.
Modbus and MQTT Test are bounded TCP-reachability probes, not protocol
handshakes. Their positive detail and evidence therefore claim only that the
resolved port accepted a TCP connection. Modbus accepts a host, IPv4 address,
or bracketed IPv6 address with an optional port and defaults an omitted port
to 502. MQTT target syntax and default ports are specified by
32-mqtt-io.md.
An explicit valid port is retained. Empty hosts, zero or invalid ports,
userinfo, paths, queries, fragments, unsupported schemes, and ambiguous
unbracketed IPv6 are field errors before DNS or connection activity. A
resolution failure is a field error; a resolved but refused or timed-out
connection is a completed negative Test with target, resolved address, and
effective timeout evidence.
The Test timeout defaults to 500 ms and is bounded to 1 through 5,000 ms. Zero and values above the maximum clamp to the nearest bound. Negative, fractional, string, object, array, and boolean timeout values are field errors; they must not silently select the default. The effective bounded timeout is reported in evidence. Address resolution and connection attempts share that single total budget and try resolved addresses in deterministic resolver order until one succeeds or the budget expires.
OPC UA client Test accepts endpoint_url or the host alias. A bare host
defaults to opc.tcp://<host>:4840; an explicit port or path is retained
under opc.tcp://, and an already canonical URL is byte-preserved after
trimming. Security policy and mode use the OPC UA configuration vocabulary.
Authentication is anonymous or the username, user_name, and user
aliases; username authentication requires non-empty trimmed username and
password values. The trust-server-certificate flag defaults false. A target
validation failure is field-specific and starts no handshake. A completed
handshake failure retains its stable OPC UA error classification and
credential-free endpoint/security evidence.
Any non-empty nested password, auth_token, token, secret, or
client_secret value requires the exact server-observed
trusted_same_host credential channel. Empty secret strings do not create a
credential. A blocked Test has no evidence or protocol error, never echoes
request parameters, and returns only a secret-presence-safe field error.
Communication capability health and fleet topology runtime, endpoint, and
link status are versioned product-projection vocabularies, not the
connector-report health vocabulary in
Connector Status. Capability health is one of
not_in_build, not_configured, simulate, runtime_unreachable,
connected, degraded, error, or configured_policy. Consumers must
interpret values within their response type and reject unknown values rather
than treating them as healthy.
The capability response order is ads, ads_server, opcua,
opcua_client, modbus_tcp, mqtt, openot, discovery, mesh,
realtime_t0, runtime_cloud, ethercat, gpio, simulated, and
loopback. Capability IDs, health values, platform values, and next-action
kinds are closed serialized vocabularies. Platform is unix for OpenOT and
EtherCAT, linux for GPIO and realtime T0, and absent for protocols without a
platform restriction.
Capability booleans and health are coherent. operational = true requires
built = true, configured = true, and health = connected; connected
likewise requires all three booleans true. not_configured requires
configured = false and operational = false. not_in_build requires
built = false and operational = false, while configured may remain true
because persisted configuration and build availability are independent.
configured_policy requires configured = true and operational = false.
Configured connectors with unavailable live evidence report degraded or
configured_policy, never connected.
An I/O capability matches its canonical driver name case-insensitively and
accepts the schema spelling alias with hyphens and underscores normalized.
When multiple live instances belong to one protocol, the projection is
conservative across the complete matching set: any faulted instance yields
error; otherwise any degraded instance yields degraded; only an all-healthy
non-empty set yields connected. Detail text must not hide a worse matching
instance merely because a healthier instance was registered first.
ADS client capability maps healthy to connected, faulted to error,
disabled to not_configured, and degraded, not_ready, or unknown to
degraded. OPC UA client capability is not_configured without configured
connections, degraded when configured status is unavailable,
configured_policy before a live read exists, error if any connection is
faulted, connected only when every connection is connected with zero
degraded points, and otherwise degraded. Build-disabled projections retain
the independent configured fact but are never operational.
The next-action projection is deterministic. Connected health uses none;
not-in-build uses get_build_with_feature; simulation uses
switch_to_online; runtime-unreachable uses open_runtime_pane;
not-configured and configured-policy use setup; and degraded or error uses
diagnose_ads for ADS and setup for other protocols. Action labels are
non-empty.
Read-only communication audit details are recursively credential-free.
Credential key matching is ASCII-case-insensitive and covers password,
auth_token, token, credential, credentials, secret, and
client_secret at every object depth, including objects inside arrays.
Non-secret protocol, target, scope, snapshot-metadata, and selection fields are
preserved. Audit sanitization never replaces a secret with its value, embeds it
in detail text, or relies on the caller placing credentials at a particular
depth.
Communication apply mutation and security¶
Server-observed transport identity determines whether a credential channel is trusted; a caller cannot self-assert trusted status. Secret-bearing input on an untrusted channel is rejected before mutation. Responses, diagnostics, and audit records may report secret-field presence but never secret values. Authorization and complete request validation precede mutation. Field errors are stable and cause no configuration write to be attempted.
Dry-run performs the same validation and lifecycle planning without writing or
deleting files and without returning a secret-bearing snippet. Add, upsert,
edit, remove, and disable preserve unrelated instances, project comments
outside rewritten driver/safe-state items, and safe-state values unless an
operation explicitly owns that data. Edit requires
one exact selected instance. Instance IDs are protocol-qualified; missing,
stale, cross-protocol, and wrong-slot IDs fail closed. Repeating an identical
edit is byte-identical. Disable retains the selected instance with
enabled = false so topology can show it as disabled.
A valid single-driver file may migrate to canonical multi-driver form when
another driver is added. Empty, comment-only, and safe-state-only files are
valid authoring inputs, but successful output must satisfy runtime activation
validation. Removing the final project driver preserves the project file,
project comments outside rewritten driver/safe-state items, and exact
safe-state values, writes an explicit driver = "none" with empty parameters,
and must not activate system-I/O fallback. A later Add replaces that sentinel
while preserving the retained project policy.
Runtime-backed protocols update runtime.toml and their owned sidecars as one
validated authoring operation. Audit records include operation, protocol,
result, dry-run state, and secret presence without secret values.
The runtime-file authoring boundary is project-confined and transactional.
Every configured sidecar path is trimmed, must be relative to the selected
project root, and must remain below that root after lexical normalization and
existing-parent canonicalization. Absolute paths, empty paths, .. escape,
and symlink escape are rejected before any directory or file is changed.
runtime.toml and every owned ADS or OPC UA sidecar are rendered completely
in memory, parsed by their production configuration parsers, and staged before
publication. A successful operation replaces the complete output set; a
failure staging, validating, creating, writing, syncing, renaming, or removing
any member preserves the complete pre-operation set. It must never publish a
new runtime.toml that points to an absent, stale, or partially written
sidecar.
Dry-run and Validate perform the same normalization, path-confinement,
rendering, and production-parser checks but create no directories, temporary
files, sidecars, or runtime file. Add, Edit, and Upsert are byte-idempotent for
identical normalized input. For protocols with an enabled field, Disable
retains the existing owned configuration and changes only that field; Runtime
Cloud, whose absent section denotes disabled policy, removes its cloud
section. Remove deletes the runtime section and its owned sidecar in the same
transaction; a missing sidecar is an idempotent success. Malformed or
unreadable existing input fails closed without replacing it. Unrelated runtime
sections, comments, and ADS-server clients not supplied by an update remain
intact. No failure response, audit entry, temporary-file name, or persisted
document includes a secret value.
Offline communication operation and fleet-topology truth¶
Offline schema, apply, and topology require no live runtime and must not invent
live evidence. Offline responses remain schema version 4. ADS authoring writes
coordinated runtime.toml and ads.toml; OPC UA client authoring writes
coordinated runtime and sidecar state, retains disabled configuration on
disable, and removes the selected section and sidecar on remove. Configured
OpenOT appears as configured evidence without a live payload. Offline results
exclude passwords, tokens, source allowlists, and other secret-bearing fields.
Offline topology requires a readable valid runtime.toml. A missing optional
io.toml is an empty configured-driver set; a present malformed or unreadable
file is an error rather than an empty topology. An enabled ADS or OPC UA client
pointer requires its readable valid project-confined sidecar. Disabled pointers
do not read or require their sidecars. Relative sidecars resolve from the
selected project; absolute paths and normalized or symlink escapes reject
before a read.
The offline host and runtime are configuration evidence: source is config,
runtime mode is stopped, health is configured_policy, and host/runtime live
timestamps, runtime load, endpoint live payloads, and host temperature, uptime,
and load are absent. Configured service and enabled-driver endpoints remain in
configuration order with their offline detail and policy/disabled health.
Offline links are sorted and deduplicated by stable link ID. A parser or
sidecar failure reports the owned file and reason but never includes a secret
value or publishes a partial topology.
Fleet topology identity and roles come from the selected project and current
runtime evidence. Configuration determines runtime identity, endpoint roles,
counterparts, stable link IDs, and external-node relationships. Merely enabling
a service projects configured_policy, not a live connection. Live values and
connected status require bound runtime evidence; degraded and faulted evidence
retains its detail. Every link reports stable ID, source, destination, protocol,
role, direction, and evidence-derived status. Fleet output never serializes
MQTT, mesh, ADS, OPC UA, or other credentials.
Fleet topology schema version 4 contains hosts, links, shared, and
external arrays and omits an empty discovered array. IDs are deterministic
ASCII projection keys. The common sanitizer lowercases ASCII alphanumeric
characters, replaces each other character with a hyphen, trims boundary
hyphens, and returns unknown if no alphanumeric character remains. Local host
identity prefers the first non-empty normalized value from HOSTNAME,
COMPUTERNAME, and the operating-system hostname; normalization trims
whitespace and trailing DNS dots. A container identity is the first
reverse-scanned cgroup component containing at least 12 hexadecimal characters,
or the hexadecimal component of a docker-<id>.scope record, truncated to 12
characters. Short, non-hexadecimal, and empty components do not become
container IDs.
Configured host IP projection parses valid listen values as hosts, removes
unspecified wildcard addresses, sorts and deduplicates the result, and falls
back to 127.0.0.1 only when no concrete host remains. Bracketed IPv6 is
unbracketed in the host inventory. Whenever an IP address is combined with a
port for an endpoint, IPv6 is bracketed so the result is an unambiguous socket
authority; IPv4 and hostnames use host:port.
Discovery timestamps floor nanoseconds to milliseconds. An entry is stale only
when its age is strictly greater than 120,000 ms; future timestamps remain
fresh by saturating age at zero. The newest observed timestamp is the maximum
over the complete entry set and is absent for an empty set. An entry is self
when either its runtime name or discovery ID equals the current runtime ID and
is excluded from discovered-host and discovered-node projections.
Discovery-derived protocol order is discovery, then web, mesh, and
control when those advertised surfaces exist. Discovery IDs use the
sanitized runtime name for runtime nodes, the host-group when supplied for
host nodes, and otherwise the discovery ID. Shared host identity requires a
matching non-empty host group on a self entry; name similarity or address
overlap alone does not claim same-host placement.
Configured I/O endpoints retain configuration order and use the configuration
index in their stable endpoint ID. The first protocol instance omits an index
suffix; later instances include their zero-based configuration index. Driver
protocol normalization trims and folds ASCII case, maps hyphens to
underscores, and canonicalizes both modbus-tcp and modbus_tcp as
modbus_tcp. Roles are client for Modbus TCP and MQTT, master for
EtherCAT, and owned_driver otherwise. Display names use the trimmed address
or broker when present; otherwise the first configured instance uses the
protocol display name and later instances append their one-based ordinal.
An enabled configured I/O endpoint binds to the corresponding enabled runtime
health row while disabled configured rows consume no live-health ordinal.
Healthy, degraded, and faulted driver evidence map to connected, degraded,
and error with the underlying detail retained. Missing health is
configured_policy; a disabled endpoint is disabled, has no live sample,
and does not advertise Test. Test support is true exactly for an enabled
canonical Modbus TCP or MQTT endpoint, regardless of accepted hyphen,
underscore, or ASCII-case spelling. Health-only endpoints use the same
canonical protocol, role, health, ID, and Test rules.
I/O snapshot live evidence exists only when both a snapshot and a nonzero
observation timestamp exist. It reports exact input, output, and memory counts
and at most eight deterministic samples in input-then-output-then-memory order.
Each sample retains direction, optional name, canonical IEC address, and either
the debug-form scalar value, an error object, or unresolved. Bit, byte, word,
double-word, long-word, fixed-byte, and wildcard addresses preserve area and
size without inventing a bit coordinate for non-bit values.
Fleet endpoint parameters recursively preserve the TOML JSON shape while
redacting secret or access-control values at any depth, including tables inside
arrays. Credential matching is ASCII-case-insensitive and covers password,
auth_token, token, credential, credentials, secret,
client_secret, and private_key. ADS source pins and allowlists
(source_ip, source_cidr, allowed_clients, and clients) are likewise
redacted rather than exposed as editable topology data. A redacted value is the
literal non-secret marker <redacted>; no sibling value or container shape may
reveal the original credential.
ADS and OPC UA client endpoint projections retain the configured connection denominator and order. ADS parameters expose route name, target identity, host, AMS port, whether a local identity is set, transport class, automatic route policy, and point mapping, but never a route credential. Symbol points and index-group points remain distinct. OPC UA parameters expose endpoint, security policy and mode, authentication class, a Boolean username-presence fact, trust policy, timing, and point mapping, but never username credentials or a password. Read, write, and read-write access serialize exactly and the writable flag follows access rather than live status.
Live connection evidence matches ADS configuration by exact connection name
or target AMS Net ID plus AMS port, and OPC UA configuration by exact
connection name or endpoint URL. Unmatched live rows cannot upgrade a
configured connection. Exact name identity takes precedence over a fallback
target or endpoint match so one stale row cannot cross-bind two configured
connections. A missing report or a report with no matching row is
configured_policy; a matching fault dominates as error; otherwise any
non-connected state or degraded point yields degraded; only a non-empty set
where every configured connection has a matching connected row with zero
degraded points is connected. A partial match is therefore degraded rather
than connected or configured-only.
ADS live state labels come from the ADS diagnostic contract. OPC UA state
labels are disabled, configured_policy, connecting, connected,
reconnecting, stale, and error. Per-connection topology health maps
connected with zero degraded points to connected, faulted to error,
connected with degraded points and all transitional live states to
degraded, and disabled/configured to configured_policy.
Live summary counts, connection arrays, and freshness timestamps are computed
only over the configured connections and their matched status rows. Extra or
stale status rows from an earlier configuration do not affect connected,
total, or last_seen_ms. Each configured connection remains present even
when unmatched, with null live fields. Freshness is the maximum matched
per-connection or per-point observation; when a report exists but supplies no
timestamp, the projection may use the projection time but must not claim an
unmatched row's timestamp.
Fleet link IDs are
link:<sanitized-protocol>:<sanitized-role>:<sanitized-from>:<sanitized-to>.
The link retains its unsanitized source, destination, protocol, role,
direction, detail, same-host fact, status, and security fact separately.
Configured links remain present while disabled or while live evidence is
missing; their status is respectively disabled or configured_policy.
Healthy, degraded, and faulted driver evidence maps to the same link status as
its endpoint.
Configured Modbus targets project an outbound client link to an external device
identified by the trimmed address. Configured EtherCAT adapters project an
outbound master link to an external fieldbus segment identified by the trimmed
adapter. Empty or non-string targets do not create external nodes or links.
Modbus link security follows its Boolean TLS configuration; EtherCAT does not
claim transport security. MQTT brokers project a publish-subscribe link to one
shared broker node per exact trimmed broker address, sorted deterministically,
with the runtime listed once in used_by; broker security follows its Boolean
TLS configuration.
ADS and OPC UA client links retain configured connection order and remain
configured_policy without a matching status row. ADS security follows the
route transport, and OPC UA security is false only for policy none. ADS
external identity uses the target AMS Net ID; OPC UA external identity uses the
sanitized endpoint URL. Their link status follows the matching per-connection
health contract above and never derives from an unrelated status row.
Configured mesh targets are configured_policy without mesh evidence,
connected only when the exact target or its embedded peer identity is in the
live peer registry, degraded when the mesh session is ready but that peer is
absent, and otherwise configured_policy. Merely having a configured target
does not prove a live mesh link.
Shared MQTT nodes are grouped by exact trimmed broker address in lexical order. Duplicate configured drivers contribute the local runtime ID once. Drivers without a non-empty broker create neither a shared node nor a shared link.
External-node projection order is configured mesh targets, unresolved runtime cloud targets, observed discovery peers, configured driver targets, ADS targets, and OPC UA targets. IDs are unique in the result and the first authoritative source for an ID wins. A runtime-cloud link with an explicit managed target does not create a fabricated external placeholder; an empty target creates the stable indexed policy placeholder. Self-discovery entries are excluded from external, discovered, host, runtime, endpoint, and link populations.
Each non-self discovery peer appears once in the discovered inventory with
its exact addresses, advertised optional control/web/mesh fields, stable
protocol order, observed direction, adopted = false, discovery source, and
floored last-seen timestamp. Duplicate observations with the same discovery
identity collapse without reordering the first observation.
Discovered entries sharing a non-empty host group coalesce into one host node.
That host retains first-observation order, a sorted deduplicated IP union, the
newest host timestamp, and one runtime child per unique runtime identity in
observation order. Entries without a host group use independent
discovery-identity hosts. A discovered runtime is connected while fresh and
runtime_unreachable while stale. Advertised web and mesh endpoints are
observations, not listener probes; web follows runtime freshness, while mesh is
connected only with ready matching mesh evidence and otherwise
configured_policy. Discovered endpoints are unowned and never advertise
Test.
6.8.1b ADS CLI authoring and server-control contract¶
trust-runtime ads is the scriptable ADS authoring, validation, route, and
server-control surface. Offline import canonicalizes cached symbol snapshots,
generates deterministic ST, creates missing parent directories, reports an
unchanged byte-identical output without rewriting it, and refuses to replace
different generated content unless --force is explicit. Offline validation
requires a snapshot and fails visibly for missing symbols, type mismatches, or
generated-source drift. Live validation rejects cached snapshots and obtains
fresh symbols from every configured connection before applying the same
compatibility check.
The ADS parser preserves every selected live-client action and its explicit
target, target Net ID, connection, configuration, guarded-write, and JSON
options. When --ams-port is omitted, client discovery and Doctor select AMS
port 851. ADS server status and symbols preserve their endpoint,
authentication, and JSON choices without selecting another server action.
Discovery, browse, Doctor, guarded route addition, live symbol import, and live
validation require the ads-wire feature. A build without that feature fails
before synthesizing hardware results or writing generated files. Doctor write
probe options are all-or-none, accept only the documented scalar types, and do
not weaken the guarded write/read-back/restore policy. Route-add credentials
are accepted only through non-empty standard input and are never echoed in
reports or failures. Route-script and route-remove remain credential-free,
deterministic artifact generation.
ADS server status, symbols, and Doctor commands use the same versioned control
response rule as trust-runtime ctl: the response must contain Boolean
ok = true before either human or --json output is treated as success.
ok = false and a missing Boolean ok return non-zero and preserve the server
error text; choosing JSON output must never turn a rejected control request
into success. External Doctor proof metadata is accepted only when kind and
name are supplied together. Human and JSON modes render the same underlying
successful report or artifact.
ADS browser-to-control boundary¶
Every ADS HTTP route delegates to its exact registered control operation and returns that operation's response; web routing alone does not manufacture ADS success. Local web authentication may supply the configured internal control token only to the in-process control dispatch that serves the authenticated request. The generic control proxy must not forward that token in headers, payload, logs, or error text to another target.
Credential-channel classification is derived from the observed request.
Loopback is trusted_same_host; a non-loopback request is
trusted_https_admin only when TLS is active and the authenticated role is
Admin; every other network request is untrusted_plain_http_network. The
server overwrites any caller-supplied channel before ads.route_add policy is
evaluated. Route-add responses and serialized failures never echo the supplied
username, password, or internal control token.
The runtime-host ADS setup page ships the Beckhoff ADS Setup,
This runtime host, Open IDE Deploy, ADS Server, and
Expected AMS Net ID onboarding labels, links to /ide, and exposes the
/api/ads/status and /api/ads/server/status routes from its local script.
It does not expose a runtime selector, a runtime target field, or a sample
MAIN.Temperature binding because the page configures the runtime host that
serves it.
Static setup-page asset checks establish only that this reviewed onboarding surface and its route names are shipped. They do not prove rendered interaction, authorization, control dispatch, ADS transport, or production readiness.
ADS AMS/TCP frame codec¶
The ADS server's stream boundary uses the six-byte AMS/TCP prefix followed by
the AMS frame. The prefix contains two zero reserved bytes and a little-endian
u32 length. That length and the configured max_frame_bytes limit cover the
32-byte AMS header plus payload and exclude the six-byte prefix.
A valid parsed frame preserves target and source AMS Net IDs and ports, command ID, state flags, header data length, AMS error code, invoke ID, and payload. Parsing rejects a prefix shorter than six bytes, nonzero reserved bytes, a declared AMS length below 32, a declared length above the configured cap, a frame shorter than its declared length, an unsupported command ID, request state flags without the ADS command bit, or a header data length that does not equal the payload length. The codec must reject those boundaries before inventing missing bytes or accepting an inconsistent payload.
Serialization requires the header data length to equal the payload length and emits the canonical prefix, header, and payload bytes. A response header swaps the request source and target endpoints, preserves command and invoke correlation, sets the response state, and records the supplied payload length. This codec contract does not by itself prove socket lifecycle, authorization, command dispatch, or external TwinCAT interoperability.
ADS server command dispatch and resource safety¶
The ADS server evaluates the client allowlist before decoding or executing a
command. A rejected client receives AccessDenied, emits one policy-rejection
audit event, and cannot allocate handles, register notifications, read runtime
values, or enqueue writes. Accepted commands require their exact ADS request
shape; truncated, extended, or internally inconsistent payloads return the
corresponding size/data error without guessing missing fields.
Direct, by-name, by-handle, system-symbol, upload, and sum-up reads preserve
the ADS group/offset distinction and the requested wire length. Symbol handles
and notification handles are non-zero and unique for the connection. Resource
limits fail with NoMemory or InvalidSize before inserting state; numeric
handle wrap searches for an unused value and must never replace a live handle.
Release or delete succeeds only for a live handle.
A direct or sum-up write reaches the runtime write port only for a resolved symbol with Write capability and emits an accepted or rejected write audit. Before a sum-up write or sum-up read/write performs its first item, the server validates the complete aggregate header, every declared write-data range, the exact aggregate length, the item limit, and the total write-byte limit. A malformed later item therefore cannot leave an earlier runtime write queued. After framing is valid, item-level address/access failures remain independent and are returned in their documented per-item result slots.
Device notifications require a receiver endpoint, a supported cycle or
on-change mode, a resolvable symbol/system target, and a non-zero watch length
no larger than that target. Notification time values that are exact ADS
100-nanosecond multiples of one millisecond are normalized to milliseconds.
Deletion invalidates only the named live registration. Unsupported
WriteControl and client-sent DeviceNotification commands remain
side-effect-free and return ServiceNotSupported.
Notification sampling clamps the requested cycle to the configured minimum.
SERVERCYCLE emits every due sample even when its bytes are unchanged.
SERVERONCHA emits the first due sample, suppresses later equal bytes, and
emits again after a byte change. Symbol values, little-endian symbol-source
versions, and static system bytes are truncated to the registered watch
length. Static system-byte targets do not read runtime value storage.
A failed or undersized source read emits an invalidated sample that preserves
the notification handle and contains zero value bytes; it does not fabricate a
value. For representable inputs, sample timestamps use Windows FILETIME ticks:
(unix_ms + 11_644_473_600_000) * 10_000. Recovery after an invalidated
on-change sample, timestamp overflow, clock rollback, sampling at Unix time
zero, and notification-handle reuse remain explicit behavior gaps rather than
being inferred from the current focused tests.
Service-port routing is evaluated relative to the configured runtime, system,
router, and TCOM ports. An unknown target port returns AMS AccessDenied with
no payload. System service port 10000 accepts ReadState, responds from port
10000 with ADS state 5 and device state 0, and rejects symbol Read with
ServiceNotSupported. Router port 1 reports device-info version 3.1 named
TCROUTER; an unknown router metadata group/offset returns AMS transport
success carrying ADS InvalidOffset and zero data length.
A notification registration binds its receiver AMS Net ID and port when the registration is accepted. Later requests multiplexed on the same TCP connection cannot retarget that registration. The router TCP/IP metadata entry-length rule remains unresolved: current behavior may return one complete 48-byte entry for a 40-byte request, while the general read contract preserves requested wire length. No catalog claim is made for that conflict.
ADS server runtime publication and lifecycle¶
ADS server publication is a truST runtime product contract outside IEC 61131-3. A default server configuration is disabled and carries the public, bounded defaults for ADS port, symbol/client/subscription/frame/sum-up/write limits, string-byte limit, read/idle timeouts, and minimum notification cycle. Those defaults are respectively ADS port 851, 256 symbols, 8 clients, 64 subscriptions per client, 256 total subscriptions, 65,536 frame bytes, 512 sum-up items, 8,192 write bytes, 4,096 string bytes, 5,000 ms read timeout, 60,000 ms idle timeout, and 50 ms minimum notification cycle. The default does not acknowledge insecure transport, enable writes, permit unpinned clients or public binds, or expose, write, or allow any configured entry.
Starting the enabled TCP service on an unavailable bind address returns the server bind-error class and no usable server handle.
An empty runtime.ads_server.expose publishes no symbols. Otherwise exposure
and writable globs match either the canonical ADS name or the bare runtime
global name, and an invalid glob fails publication rather than broadening it.
Publication considers the complete eligible runtime-global set, orders it by
canonical ADS name, and only then applies max_symbols; storage insertion
order must not choose the bounded subset. The v1 supported scalar descriptors
are BOOL, signed and unsigned 8/16/32/64-bit integers, REAL, LREAL, and
BYTE/WORD/DWORD/LWORD. A symbol is always readable and becomes writable
only when both the global write gate and a writable glob authorize it.
Runtime values for which descriptor conversion returns no supported descriptor
are omitted without suppressing supported sibling symbols; this omission rule
does not select the unresolved STRING or array descriptor policy.
Namespace mapping is exact and reversible between <namespace>.global.<name>
(or global.<name> without a namespace) and its runtime global; a foreign
namespace is not stripped.
The server-owned symbol-table builder sorts accepted names, rejects duplicate names, preserves declared capability flags, and assigns contiguous offsets from the configured index group using each descriptor's byte size. The same accepted symbol set therefore produces the same snapshot regardless of input order. Deterministic snapshot serialization is stable for an unchanged snapshot. The local symbol-version counter establishes version 1 on first observation, retains it while the complete serialized snapshot is equal, and advances when that serialized snapshot changes. This counter contract does not prove atomic live publication.
Each value read obtains a runtime snapshot from the configured provider. No
snapshot returns ADS NotReady; a foreign ADS name or absent runtime global
returns NotFound; an unencodable value returns InvalidData; and a byte-size
mismatch returns InvalidSize. Those failures return no fabricated value
bytes. The clock authority and threshold for classifying an available snapshot
as Good or Stale remain unresolved under
SPEC_GAP_P18_ADS_SERVER_SNAPSHOT_FRESHNESS_CLOCK_001; existing freshness
observations are not specification authority.
A disabled lifecycle returns no server without consulting the snapshot
provider or binding sockets. An enabled lifecycle requires a concrete bind IP
and AMS identity, initializes its symbol source from the available runtime
snapshot or an empty not-ready snapshot, and owns both the ADS TCP listener and
UDP identify responder. Runtime status accessors report the live addresses,
policy, client count, and served symbol table. Refresh retains the bound socket
ownership, while explicit shutdown and drop stop both listeners. The atomic
publication and version transition for a refreshed symbol table remains
unresolved under SPEC_GAP_P18_ADS_SERVER_SYMBOL_VERSION_ATOMICITY_001 and is
not test authority in this contract.
The capacity and descriptor policy for runtime STRING values remains
unresolved under SPEC_GAP_P18_ADS_SERVER_STRING_DESCRIPTOR_POLICY_001; the
public string-byte configuration bound alone does not select that behavior.
ADS server client policy, write-back, and audit¶
An ADS server client is authorized only when its AMS Net ID matches an
allowlist entry and its observed source matches that entry's parsed exact IP or
valid IPv4/IPv6 CIDR. Equivalent textual spellings of the same IP address are
equal; invalid IP or CIDR text and address-family mismatch fail closed. An
Unpinned entry remains AMS-Net-ID-only and is effective only while
allow_unpinned_clients is explicitly true, including when configuration is
constructed programmatically; when opted in, it does not require an observed
source. Missing source identity denies pinned Ip and Cidr entries, and
poisoned policy state denies all entries. A temporary permit authorizes only
its exact AMS identity and parsed source IP and is removed when its guard
drops.
Denied attempts retain bounded operator evidence. Repeated attempts with the same AMS identity, source, and reason coalesce by increasing the count and last seen time; distinct attempts retain at most 32 records and evict the oldest. Reasons distinguish an unknown AMS identity, a missing source, and a source outside the configured pin, and the suggested entry preserves the observed identity without authorizing it.
Runtime write-back checks, in order, the live client policy, the global write
gate, combined exact-namespace and writable-glob authorization, runtime
readiness, and ADS value decoding before it queues a global write. A foreign
namespace or a symbol outside the writable allowlist returns AccessDenied
before runtime readiness is inspected; a stopped runtime cannot change that
security-first result to NotReady. Ready, Running, and Paused resources
may accept an otherwise valid write. Boot, Faulted, and Stopped
resources, or any resource carrying a last error, return NotReady only after
the earlier authorization checks pass. Malformed ADS value bytes return
InvalidData. A malformed writable glob rejects write-port construction with
InvalidParameter. Every rejection is free of a queued PLC mutation. Accepted
writes use the runtime debug queue's normal same-target coalescing behavior.
The audit adapter emits one runtime control-audit record per accepted write,
rejected write, or policy rejection when a sink is configured. It preserves
the event timestamp, client AMS identity and observed source, symbol and value
type when present, stable ads.server.write or ads.server.policy request
type, stable kind, success state, and ADS error code/message. Audit identifiers
are process-local unique records; an absent or disconnected audit receiver must
not change ADS command behavior.
ADS diagnostic report and production-readiness truth¶
ADS Doctor reports use diagnostics schema version 2 and retain the documented snake-case or kebab-case wire names for roles, vantages, transports, steps, statuses, skip reasons, actions, connection states, and readiness reasons. Evidence and action maps serialize in deterministic key order. The report and route-plan schemas contain no credential, username, password, token, or other secret-bearing field; remediation may identify a credential channel but must not echo its contents.
A Doctor report is fail when it has no steps or any production-blocking step
is not pass. Otherwise any warning, skip, or explicitly non-blocking failed
step makes the report partial; only a non-empty set of passing steps makes it
pass. A report is never production-ready without both an overall pass and
role-appropriate production evidence. Client evidence binds the runtime and
target identities. Server evidence binds the runtime identity and allowed
clients, and additionally requires independent external-client proof marked as
verified with kind twincat (ASCII-case-insensitive), a non-empty client name,
and a client timestamp. Loopback self-test or pyads evidence remains useful
diagnostic proof but cannot establish TwinCAT production readiness.
Production evidence hashes the canonical runtime identity, target identity or
allowed-client set, ADS configuration, symbol snapshot set, optional generated
ST, deployed ADS configuration, and the live ADS status used by the Doctor.
Symbol-snapshot hashing is independent of connection ordering. A later
readiness evaluation is not_ready when Doctor evidence is absent. With
evidence present it is needs_recheck when live runtime status is absent, the
Doctor did not bind a live-status hash, deployed configuration evidence is
missing or mismatched, the current status hash changed, runtime ADS is not
healthy, the runtime clock was reported unreliable, or the evidence is older
than its declared freshness bound. The effective expiry is the earlier of an
explicit expiry and doctor_timestamp_ms + stale_after_ms; arithmetic overflow
must not extend evidence lifetime. Only evidence with none of those conditions
is ready.
ADS onboarding identity authority¶
ADS onboarding identity is a truST product contract outside IEC 61131-3. The
selected runtime-host source address and the selected target address must each
be a syntactically valid IP address before they can become route, Doctor, or
symbol-import authority. An omitted local AMS Net ID is derived only from a
canonical IPv4 source as <ipv4>.1.1; IPv6 requires an explicit override. An
explicit override, including an explicitly blank value, must contain exactly
six decimal octets in the range 0 through 255 and is stored in canonical
decimal form. A malformed source address, target address, or override is
rejected before an onboarding identity is returned and before any ADS wire or
route side effect. Public and NAT-suspect identities remain ineligible for
automatic route creation.
ADS onboarding discovery endpoint identity¶
ADS onboarding discovery accepts a manual target, directed UDP identify, and optional directed-broadcast results. Manual identity does not depend on UDP when an AMS Net ID is supplied; without one, directed identify supplies the target identity. A failed broadcast candidate does not erase successful manual or other broadcast results.
Discovery uniqueness is the ADS endpoint identity (AMS Net ID, AMS port),
not the host IP address alone. Multiple PLC runtimes may share one host IP and
must remain separate results when their AMS Net ID or AMS port differs.
Repeated observations of the same ADS endpoint are reported once even if its
IP or discovery provenance differs. Directed-broadcast targets are derived
only from usable non-loopback, non-link-local IPv4 interface candidates and
are themselves deduplicated.
ADS onboarding live-wire and guarded-write recovery¶
The ads-wire onboarding boundary performs real UDP identify, TCP 48898,
route-back, AMS target, state, symbol, handle, sum-up, notification, guarded
write, and route-add operations without exposing raw ADS types in the public
Doctor schema. UDP identify collection ignores malformed replies but retains
every distinct (AMS Net ID, AMS port) endpoint even when multiple runtimes
share one host IP. Repeated observations of that same endpoint are emitted
once, and a receive window with no valid reply fails explicitly.
The live ADS transport cache is scoped to target IP, target AMS Net ID, target AMS port, and selected local AMS Net ID. Changing any component discards the transport plus cached symbols and handles before reconnecting. Symbol upload replaces the name cache; handle resolution may refresh that cache but must fail for an absent symbol. Sum-up reads reject unknown local handles, non-good quality, missing values, unknown returned point names, and value/type encoding failure instead of manufacturing bytes. Notification setup resolves the named symbol and subscribes in on-change mode.
A guarded write probe first requires a writable remote symbol whose complete type descriptor matches the requested descriptor, resolves its handle, and reads the original value. Once the probe write has been attempted, every write error, read-back error, or read-back mismatch must attempt to write and verify the original value before returning failure. A restore failure is retained in the returned diagnostic alongside the initiating failure. A successful probe is complete only after both probe read-back and restored-value read-back match. Automatic route addition continues to use the already validated runtime-host AMS identity and one-shot credentials; the transport never enables implicit route creation.
ADS client configuration and activation¶
ADS client configuration and activation is a truST product contract outside IEC 61131-3. Configuration parsing must fail closed before worker creation or any ADS wire activity unless all connection names are unique; every connection name and host is nonblank; every target AMS Net ID and explicit local AMS Net ID contains exactly six canonical decimal octets; every target AMS port is non-zero; and every connection declares at least one point. Plain transport requires explicit insecure-transport acknowledgement, and that acknowledgement is invalid for any non-plain transport.
Across the complete configuration, every declared global variable binding is unique. Each point supplies exactly one complete address form: either one nonblank symbol or the complete index-group, index-offset, and non-zero size tuple. Notification options are valid only for notify-mode points, and every point's IEC type, STRING capacity, array dimensions, address size, access, and update-mode metadata must be internally valid. Production-readiness activation additionally requires every configured local AMS Net ID to equal the selected runtime-host identity; a missing or mismatched local identity is not production-ready.
Secure transport remains an unresolved product decision: the configuration
schema currently recognizes a reserved secure value while the implemented
ADS transport rejects secure activation. This specification does not select a
new parser or transport behavior for that contradiction, and the contradiction
is not test authority until a separate product decision resolves it.
ADS core value codec and descriptor size¶
The shared ADS value codec is a truST product contract outside IEC 61131-3.
For the reviewed fixed-width scalar matrix, BOOL uses one byte with reviewed
true encoded as 1; signed and unsigned integers and bit strings use their
declared one-, two-, four-, or eight-byte width; and REAL and LREAL use
four and eight bytes respectively. Multi-byte values use canonical
little-endian byte order. The reviewed matrix proves exact encode/decode
round trips for its named representative values; it does not select behavior
for noncanonical BOOL bytes, every elementary value, or non-finite egress.
Non-finite ingress remains governed by the ADS client configuration and
activation contract.
For the reviewed STRING(8) partition, the declared capacity is eight payload
bytes and the fixed layout is nine bytes including the terminator. Decoding the
reviewed zero-padded buffer returns bytes through the first NUL, and encoding
Pump produces the exact trailing-zero-filled nine-byte buffer. This partition
does not select Unicode capacity, invalid UTF-8, an absent terminator,
over-capacity values, or a missing descriptor length.
For the reviewed one-dimensional INT[1..3] partition, decoding and encoding
preserve the inclusive bounds, element order, scalar representation, and exact
total byte extent. Multidimensional order, invalid or overflowing bounds,
element-count overflow, and array element-type failures remain outside this
contract.
The reviewed mismatch boundary rejects an input byte slice whose length
differs from the descriptor as ByteLengthMismatch, a scalar value supplied
for an array descriptor as ValueTypeMismatch, and an array whose bounds
differ from the descriptor as ArrayShapeMismatch. These observations do not
establish a complete precedence order among all possible mapping failures.
ADS symbol metadata is admissible at the reviewed descriptor-size boundary
only when the endpoint byte_size equals the descriptor-computed extent. The
reviewed WORD[1..4] extent is eight bytes and the reviewed scalar REAL
extent is four bytes. A REAL endpoint size of eight rejects as
ByteSizeMismatch with expected 4 and actual 8. Descriptor-size overflow
and every nested descriptor failure remain explicit gaps.
This shared codec authority does not select the unresolved runtime ADS server
publication policy for STRING or arrays. It also makes no wire,
external-endpoint, or TwinCAT interoperability claim.
ADS core point-quality lifecycle¶
The shared ADS point-quality record is a truST product contract outside IEC
61131-3. A reviewed cold-start status preserves its supplied point name and
starts in Stale; the cold-start partition does not establish the detail text
or a timestamp. An explicitly constructed reviewed stale record may preserve
the supplied last-good timestamp and detail.
For the reviewed in-memory transition sequence, an initial stale record has no
last-update timestamp. Marking it good sets Good, records the supplied update
timestamp, and clears stale detail. Marking that record failed sets Error,
replaces the timestamp with the failure timestamp, and records the failure
detail. Marking that error record stale sets Stale, preserves the last known
timestamp, and replaces the detail with the current stale reason.
These constructor and mutation rules do not select the clock source, timestamp units beyond the record's declared millisecond field, monotonicity, concurrent mutation, serde compatibility, worker publication timing, transport behavior, or external endpoint truth. The runtime ADS worker remains responsible for deciding when a transport or lifecycle event invokes these transitions.
ADS client transport boundary¶
The ADS client transport is a truST product contract outside IEC 61131-3. It must validate the required target AMS Net ID and an optional local AMS Net ID as exactly six decimal octets in canonical form, and validate the non-zero target AMS port, before opening TCP 48898 or performing any other wire operation. The runtime transport supports plain ADS only and must reject reserved secure transport or implicit route creation before the wire; route authoring remains an explicit onboarding operation outside this transport.
After connection authority validates and before replacing a client, the
transport attempts best-effort remote release of active notifications and
symbol handles. Remote release failure does not make remote cleanup
deterministic; the deterministic guarantee is that replacement, disconnect,
and drop tear down the transport's local handle maps, notification receiver,
and client ownership. ADS device state maps Run to runtime run, Stop,
Idle, and Config to runtime stop, Error and Exception to runtime fault,
and every other state to runtime unknown.
Symbol upload projects only supported scalar, array, and STRING metadata into the runtime descriptor model. Every projected symbol is readable; the ADS read-only flag withholds write access, and the persistent flag also establishes the runtime retain guardrail. Array dimensions, STRING capacity, and byte size must agree with the projected type. Unsupported metadata may be omitted, but inconsistent, overflowing, or otherwise unrepresentable metadata fails closed instead of producing a bindable descriptor.
A symbol-upload error establishes a missing return route when its typed kind is
RouteMissing, or when a NoSymbols failure explicitly describes a timeout,
no reply/response, route setup, or failure while receiving the reply. An actual
empty symbol-table response and a refused TCP connection are not reclassified
as route missing. This diagnostic classifier selects recovery guidance only; it
does not claim that adding a route will make the subsequent upload succeed.
By-name handle resolution first verifies that the remote symbol byte size exactly equals the requested type size and then acquires a remote symbol handle. Index-group/index-offset resolution verifies that its declared byte size exactly equals the requested type size and does not manufacture a remote handle. Replacing or removing a cached by-name handle releases the old remote handle.
Sum-up reads and writes preserve input order and cardinality. Each item retains its own conversion or ADS error as non-good quality; failed reads have no value, and failed writes are never reported as successful. The transport must not shift results between points, discard failed positions, or manufacture values to fill missing or malformed results. Subscriptions accept notify-mode points only, preserve the requested on-change or cyclic notification mode, and reject poll-mode points before subscription. Unknown notification handles and samples whose bytes do not decode as the resolved type fail closed with no manufactured value. The ADS symbol-version byte is an opaque equality token: a changed token invalidates symbol-derived state, but ordering, monotonicity, and wraparound must never be inferred from its numeric representation.
The connection worker constructs handle and subscription correlation as an unpublished candidate. Handle resolution must return exactly one response for every requested point, and every response must preserve that point's configured address and complete type descriptor. Response order is not authority, and the numeric handle value is opaque: the same numeric value used by different points is not rejected merely because of its representation, while this contract does not select a zero-handle policy for by-name resolution. Missing, extra, duplicate-point, wrong-address, or wrong-descriptor responses reject the candidate. Notify subscriptions likewise return the exact requested point, and every active subscription ID is unique within the connection; this contract does not select a zero-ID policy. The worker publishes the complete handle/subscription maps only after all candidate operations succeed.
A poll batch must have the same cardinality as its requested handle batch and
must preserve point identity at every position before any per-result cache
mutation. A structural mismatch rejects the complete poll response; a later
uniform connection fault projection may update all qualities, but no prefix of
the malformed response becomes accepted data. Each notification sample is
validated independently against both the configured notify point and that
point's currently active subscription ID before that sample mutates the cache.
Unknown points, inactive IDs, and cross-point correlations fail closed. Repeated
valid samples for one active point remain valid and are applied in delivery
order; a later invalid sample does not roll back the raw values stored by
earlier correctly correlated samples, but its connection-level validation
failure may revoke their Good authority. A structurally valid, correctly
correlated point-level non-good or non-finite result remains a per-point quality
result and does not invalidate other points in the batch.
Symbol-derived candidates are valid only when the opaque symbol-version token
is equal before and after upload, handle resolution, and subscription setup.
When the active token changes, the worker immediately makes its old handles,
subscriptions, token, and candidate state unavailable and revokes Good
authority from cached inputs before attempting refresh. Raw cached values may
remain available for diagnostics, but apply_inputs cannot apply them without
current Good quality. Revoking readable authority changes only currently
Good quality and cannot overwrite a read/write point's newer ADS write
pending or explicit non-finite output error. Numeric ordering and monotonicity
are never inferred, including across wraparound. A reconnect request or a
validation failure after a successful transport connect performs best-effort
disconnect, clears local
handles, subscriptions, symbol token, and symbol-check scheduling, and marks
affected input authority stale for transport recovery or error for validation
failure. Replacement state becomes visible atomically only after the complete
candidate passes correlation and token-stability validation. This contract does
not select whether pending outputs are replayed, preserved, or dropped across
reconnect.
Local output intents are generation-tagged. Capturing a finite changed output
atomically replaces the point's queued value with the latest value and reports
ADS write pending. A write completion may acknowledge or update quality only
when it still names the generation that was transmitted; completion for an
older value cannot remove a newer queued value or overwrite its pending/error
quality. The worker validates write-response cardinality before any per-result
acknowledgement or quality commit. A malformed response therefore performs no
partial per-result commit and leaves every transmitted generation pending. A
correlated success atomically acknowledges that generation and reports good
quality; a correlated failure terminally removes that exact generation and
reports explicit ADS write failed: ... quality without falsely acknowledging
success. A later changed finite output creates a new generation normally.
Transport-error and malformed-cardinality connection
projections compare the current complete write-generation state with the
complete snapshot captured atomically with the pending-write batch before
request construction. They must not overwrite a finite pending quality or
non-finite rejection created while the transport call is in flight, including a
change to an output point that was not in the transmitted request; other
input/read authority still becomes stale or error with the connection.
Worker shutdown and drop must wake an idle interval wait promptly, request
best-effort disconnect, publish Disconnected, revoke any remaining Good
input authority, and join the worker thread. Raw input values may remain for
diagnostics, and read/write points retain newer non-good pending or explicit
output-error quality. This local wakeup guarantee does not claim cancellation
of a transport call already in flight. Binding identity, declared runtime type,
remote descriptor, access, retain opt-in, and index byte-size validation remain
separate fail-closed partitions; success in one partition cannot substitute for
another.
The lifecycle/security invariant is that every connection attempt validates all wire authority first and performs no implicit route mutation; after successful validation, replacement, disconnect, and drop attempt best-effort remote release and deterministically tear down transport-owned local resources. The data/value invariant is that supported metadata, handles, sum-up positions, qualities, notifications, and symbol-version equality preserve the remote meaning exactly, while inconsistent or undecodable data fails closed without a fabricated descriptor, success, or value.
ADS server Doctor orchestration and loopback proof¶
ADS server Doctor orchestration and loopback proof are truST runtime product contracts outside IEC 61131-3. The Doctor reports only live, endpoint-correlated proof. A supplied status snapshot cannot make symbol service healthy when the running ADS server's live symbol table is empty; the reported point count and readiness state must reflect the live table used to serve clients.
Independent external-client evidence is verified only when its client kind and
name both contain non-whitespace text and its timestamp is present. Missing or
blank proof content must not produce a passing external-client step or set the
production-evidence external_client_verified flag. Existing production
readiness rules remain unchanged: loopback and pyads evidence are diagnostic,
while production readiness still requires verified TwinCAT evidence.
The loopback client rejects zero symbol and notification handles. A matching notification handle proves delivery only when the sample contains the complete payload requested for that symbol; a truncated matching prefix cannot pass. Every solicited loopback reply must be an ADS response frame whose source and target AMS Net IDs and ports are the exact reverse of the request and whose command and invoke ID match the request before its payload is trusted.
Every passing server Doctor step and every verified evidence field is derived from the running server's live state or from complete, nonblank, endpoint-correlated external/loopback proof; missing, zero, truncated, request-direction, or endpoint-mismatched input fails closed without manufacturing health, verification, or success.
ADS Doctor orchestration and active-device safety¶
The client ADS Doctor emits exactly the ordered required-step set owned by the runtime engine. A production-blocking failure prevents every later direct probe; cancellation marks the first not-yet-started step as cancelled and the remaining steps as blocked. Guarded writes are skipped without blocking when writes were not explicitly enabled, and enabling writes without a complete probe is a blocking configuration failure. Production evidence is attached only after every required step passes, from a runtime-host vantage, with live deployed status available.
Manual Doctor targets must contain a syntactically valid IP address, a non-zero AMS port, and, when supplied, a canonical six-octet AMS Net ID. Those fields are rejected before any ADS wire operation. An explicitly selected symbol must contain non-whitespace text and must occur in the uploaded symbol table; it is never accepted merely because a later transport happens to resolve it.
An active-device shortcut is valid only when its target IP and AMS port, plus the expected AMS Net ID when supplied, identify the requested Doctor endpoint. A mismatched snapshot fails closed without opening a second ADS connection. For a configured route whose host is an IP literal, runtime active-device lookup therefore requires that host IP and AMS port to equal the requested target and, when the request supplies an expected target AMS Net ID, requires that identity to equal the route target AMS Net ID. A route name, target display name, or one matching endpoint component cannot substitute for the complete required match. When both the configured route and request carry a selected local AMS identity, those identities must also be equal. Hostname resolution, omitted-local-identity semantics, and selection among multiple exact matches remain unresolved product decisions and establish no test authority here. Read-only active-device reports derive state only from the supplied live snapshot: a batch-read step passes only when at least one point has good quality with a recorded update timestamp; no points, no timestamped good point, or any degraded point produces a warning. The full Doctor for an overlapping active device remains blocked until the caller explicitly pauses it.
ADS onboarding route artifact and credential safety¶
Automatic ADS route addition is available only over a credential-permitted channel and never for a public or NAT-suspect runtime-host identity. Both the one-shot username and password must contain non-whitespace text before the wire operation is called. Credentials are neither serializable nor present in route plans, generated artifacts, reports, logs, or error detail.
Route-plan and route-removal artifacts are deterministic and credential-free.
XML content escapes every user-controlled field. Generated PowerShell treats
that XML only as data: it transports the UTF-8 XML payload through a non-
executable encoding and decodes it before assigning DocumentFragment.InnerXml;
user-controlled text must never be able to terminate a PowerShell here-string
or introduce a command. PowerShell single-quoted values escape embedded quote
characters, and generated filenames use a filesystem-safe slug. Apply and
remove scripts back up each selected StaticRoutes.xml, match a route by its
exact name, preserve unrelated routes, and report an encoding/BOM change.
Client and server roles retain their distinct operator instructions.
ADS symbol-import artifact safety¶
The server-control symbol-import apply operation writes only inside its selected
project root. Explicit ads.toml, snapshot, and generated-ST destinations must
be non-empty relative paths without parent-directory, root, or platform-prefix
components. The default snapshot destination is available only when the
connection name is a safe single filename component; a connection name must
never become path syntax. The three resolved artifact destinations must be
distinct. A snapshot destination must be a direct *.symbols.json child of
ads/snapshots, because that canonical directory is the authority reloaded by
subsequent imports; accepting an undiscoverable snapshot location is forbidden.
Before writing any import artifact, server control checks an existing generated
ST destination. Byte-identical next content is safe to retain. Different next
content may replace the file only when the existing file still validates
byte-for-byte against the current ads.toml and every canonical cached
snapshot; this permits a legitimate follow-up import while protecting a
drifted or operator-edited file. Missing, malformed, or inconsistent current
authority fails closed. A rejected path or overwrite preflight leaves all three
artifacts unchanged.
These confinement and overwrite rules apply to the server-control authoring
surface; the explicit filesystem paths of the local CLI remain operator-owned.
ADS symbol-import selection and generated identifiers¶
ADS symbol selection is deterministic. A non-empty exact-symbol list has
exclusive precedence over include patterns. Otherwise include patterns are
ASCII-case-insensitive; * matches any sequence, anchored prefixes and suffixes
must be honored across the complete symbol, and a pattern without * performs
the documented substring search. An explicitly empty pattern matches nothing
rather than broadening the import to every uploaded symbol. With neither exact
symbols nor include patterns, all candidates remain selected by default.
Generated local variable names normalize symbol text to lowercase letters,
digits, and single embedded underscores, receive a stable suffix on collision,
and must lex as one Structured Text identifier rather than a keyword or other
token. A normalized keyword receives the ads_ prefix. This follows IEC
61131-3:2013 6.1.2 and Table 2 for identifier form and 6.1.3 for the
case-insensitive prohibition on using keywords as variable names.
Apply remains read-only unless write acknowledgement is explicit. With that
acknowledgement, a remote symbol carrying both Read and Write capabilities maps
to read_write, while a Write-only symbol maps to write; acknowledgement
must not manufacture a remote Read capability that the symbol does not expose.
A cached snapshot is authoritative only for its exact selected connection; a different connection name is rejected before candidate generation or mutation. Cached candidate and group ordering is deterministic. Every candidate reports its proposed local name, access and polling disposition, and selected state without changing the remote symbol's capabilities. A live import is never replaced by cached or synthetic results when wire support is unavailable.
ADS symbol-import apply and merge¶
Core apply requires at least one selected candidate and parses an existing
ads.toml strictly before producing replacement artifacts. It replaces only
the imported connection with the same exact name, preserves and sorts other
connections, pins the selected runtime-host AMS identity, canonicalizes the
merged snapshot set, and generates one deterministic ST interface. Local point
names and their generated quality names are unique across every connection.
Imported ADS scalar descriptors project to the corresponding supported IEC BOOL, signed and unsigned integer, REAL/LREAL, bit-string, and STRING type names defined by Data Types. STRING capacity and every inclusive array lower/upper bound are retained. Poll and notification modes and read, write, or read-write access are serialized without silently changing the selected capability. Malformed current configuration, missing selection, unacknowledged writes, invalid descriptor metadata, or generation mismatch fails before an artifact is returned.
The generated ADS interface is one deterministic Structured Text source for
all configured connections. It declares the shared ADS_QUALITY enum exactly
once, then each configured point and its <name>_quality companion in stable
connection/point order. Generated identifiers must be valid non-keyword
Structured Text identifiers and unique across point names and generated
quality names. Snapshot and configuration descriptor byte sizes must agree
before generation. STRING capacity and inclusive array bounds are rendered
without widening or flattening their declared type.
Offline validation regenerates the complete expected source from the current configuration and canonical snapshots. It accepts only byte-identical source and reports the first differing line for stale content. A generated source that successfully compiles with its consuming project proves language/toolchain compatibility for that exact artifact; generation or source equality alone does not prove compilation, runtime binding, or live PLC communication.
Once that generated source is compiled into a runtime, its point and quality declarations are ordinary first-class globals. Their declared type and enum variant remain available through HMI schema/values, the HMI binding catalog, control evaluation, debugger globals, historian capture, and the supported OPC UA scalar/enum projection. This cross-surface rule requires runtime metadata and snapshot evidence; source-text presence alone is not proof.
6.8.1b Runtime benchmark measurement and output contract¶
trust-runtime bench validates each workload before measurement: sample,
payload, and dispatch-fanout counts are non-zero; project paths exist; and
synthetic loss and reorder rates are within the closed interval from zero to
one. Requested warmup cycles are executed before measured project cycles and
are excluded from latency samples.
For T0, mesh, and dispatch workloads, --payload-bytes is the exact payload
length exercised by every applicable measured path. Implementations must not
silently cap large requested payloads. Mesh pub/sub and query/reply both use
the requested length; dispatch embeds the requested length in its mapped
control payload. Their reports expose the effective payload length so evidence
cannot claim one workload size while measuring another.
Latency summaries use sorted nanosecond samples and nearest-rank p50, p95, and p99 selection. Histogram classification rounds each sample up to a whole microsecond and retains an overflow bucket. Jitter contains the absolute delta between consecutive observations, so fewer than two observations produce no invented jitter sample. Synthetic loss and reorder decisions use a fixed seed for reproducible workload selection.
JSON output serializes the benchmark kind and its typed report; table output
renders the same contract fields. Project reports separate measured cycles,
configured cycle budget and overruns, throughput, requested watched globals,
and optional VM/Tier-1 profile data. A missing watched global is explicit JSON
null, not an omitted or fabricated value.
Every benchmark subcommand defaults to table output when --output is omitted;
an explicit --output json selects JSON without changing the selected workload
parameters.
PLCopen CLI surface contract¶
trust-runtime plcopen export accepts an optional project, optional output
path, optional export target, and JSON-output selection. An omitted export
target selects generic. The canonical targets are generic, ab, siemens,
and schneider; the documented Allen-Bradley/Rockwell, Siemens TIA, and
Schneider EcoStruxure aliases map to their corresponding canonical targets.
trust-runtime plcopen import requires an input XML path and accepts an
optional project and JSON-output selection. Parsing preserves supplied paths
and choices without performing import or export work.
6.8.1c Runtime project-check contract¶
trust-runtime check validates a project without writing program.stbc or
another compiled bytecode artifact. An explicit --project selects the
project root; otherwise the command uses bundle auto-detection and falls back
to the current directory. A relative --sources override is resolved from the
project root, while the default source root is <project>/src.
The command validates required runtime.toml and io.toml files plus optional
ads.toml and opcua_client.toml files, inspects the complete source and local
dependency layout, and compiles valid layouts in memory. Independent
configuration and compile findings are accumulated in one response. A
configuration failure does not suppress an otherwise possible in-memory
compile, and a compile failure does not discard the source and dependency
identity already obtained from successful layout inspection.
--json and --ci emit response version 1. The response identifies the
command and project; exposes ok/status, error and warning counts, stable
issue codes and optional file locations; and reports the exact source count,
source paths, dependency roots, resolved dependency names, and optional
in-memory bytecode size. source_count equals the number of reported source
paths. ok is true exactly when the error count is zero. Human output is
derived from the same response and reports either the successful source list
or every accumulated issue.
Success exits 0. Any configuration or source-layout error exits with the
invalid-configuration class 10, including a response that also contains a
compile error. A compile-only failure exits with the build-failure class 11.
Both successful and failed checks leave an existing program.stbc untouched
and never create one.
6.8.1d Runtime build and CI failure contract¶
trust-runtime build compiles the selected project and writes
<project>/program.stbc. An explicit --project selects the project root;
otherwise ordinary bundle discovery is used and the current directory is the
final fallback. The default source root is <project>/src. A relative
--sources override is resolved from the project root, including when the
command is launched from another working directory. Local package dependencies
are resolved and compiled with the selected project sources.
Compilation completes before program.stbc is opened for writing. A source,
dependency, or compile failure therefore neither creates the artifact nor
replaces bytes from an earlier successful build. A successful build writes the
new bytecode and reports the exact collected source paths, including dependency
sources.
--ci emits response version 1 with command build, status ok, the selected
project and output paths, source_count, the complete source list, dependency
roots, and resolved dependency names. source_count equals the number of
reported source paths. Human mode reports the written path and source count,
shows at most the first five collected paths, and reports the number omitted.
CI failures use stable process classes: invalid project or configuration input
is 10, build or compile failure is 11, test assertion or runtime-test failure is
12, timeout is 13, and an unclassified internal failure is 20. A recognized
failure class is preserved regardless of command context. Timeout takes
precedence when a message contains multiple recognized classes; otherwise test,
build, and configuration classes are considered in that order. Only an
unclassified message uses the parsed command as a fallback: build and check
map to 11, test to 12, validate to 10, and other or absent commands to 20.
This CLI and process-exit policy is a truST product contract rather than IEC
61131-3 language behavior.
6.8.1e Standalone Web IDE contract¶
The built-in standalone Web IDE is a local authoring surface over the selected project root. Its shell is served from bundled, content-hashed local assets and does not require a CDN. Project selection is explicit: opening another approved project updates the active root, while a request outside the approved base is rejected before filesystem access.
An IDE session has a stable Viewer or Editor role. Viewer sessions are read-only; Editor sessions may mutate project files. Successful activity renews the sliding idle deadline, an idle session expires, and reaching the fixed session limit evicts the oldest inactive session rather than an active, recently renewed one. Pairing and session tokens are required by the endpoint contract and are never accepted as workspace paths or file contents.
All source paths are relative to the active project root. Absolute paths, parent traversal, escaping symbolic resolution, malformed include/exclude globs, and oversized JSON or file content reject before mutation. File writes use optimistic version authority: the caller supplies the expected version, a successful write increments it, and a stale expected version returns a conflict with the current version. Create, rename, and delete preserve project containment, report collisions, and append the successful mutation to the filesystem audit log. Tree, file-list, and workspace-search results use stable relative paths and honor validated include/exclude globs.
Diagnostics, hover, completion, and formatting use the same Structured Text analysis and formatting contracts as the language service. The format endpoint returns the affected path, changed flag, and complete formatted content. Build, test, and validate endpoints start an asynchronous job, return its job ID, and retain terminal output plus parsed source locations. Health reports active sessions, tracked documents, and accumulated frontend telemetry. Frontend telemetry updates the same health snapshot; collaborative-presence functionality that is not implemented is reported explicitly as out of scope, not as an active collaboration session.
Workspace path normalization trims outer whitespace, removes . components,
uses / in returned paths, and rejects an empty non-root path, absolute or
rooted input, every .. component, and every component beginning with ..
Source paths additionally end in .st case-insensitively. File and tree
enumeration omit hidden entries, return project-relative /-separated paths,
and sort tree siblings by name. Source enumeration includes only .st files
and does not follow a path outside the selected root.
Reads decode UTF-8 and admit a file whose exact byte length equals the
configured limit; one byte over returns too_large. Fingerprints contain exact
byte length and the filesystem modification time in Unix-epoch milliseconds.
Glob inputs are trimmed; absent or blank input disables that filter, malformed
syntax returns invalid_input, and a valid pattern is preserved as the
matching authority.
IDE positions count Unicode scalar values within zero-based lines. A position beyond its line clamps to that line's byte end and a line beyond the document clamps to document end. Compiler text offsets converted back to IDE positions must be valid UTF-8 boundaries. Rename edits are applied from greatest start offset to least so earlier offsets remain stable. Every range is ordered, in-bounds, on UTF-8 boundaries, and non-overlapping; invalid edit sets reject without returning partially edited text.
Completion prefix extraction considers ASCII letters, digits, and _ in the
current line before the cursor. In-scope declaration fallback recognizes valid
ASCII identifiers from POU/type/class declarations and variable declaration
left-hand sides, ignores comment-only lines, ranks prefix-matching symbols
before keywords and non-prefix matches, deduplicates labels
ASCII-case-insensitively, then applies the requested result limit.
The formatter is deterministic, uses two spaces per nesting level, removes
trailing horizontal whitespace, preserves blank lines and comment text, and
returns one terminal newline for every nonempty input. POU, declaration,
conditional, loop, case, repeat, action, transition, method, property, class,
interface, resource, and configuration openers indent their bodies. Their
corresponding END_*, ELSE, ELSIF, and UNTIL lines dedent before
rendering; branch continuations then indent their following body.
Session removal is atomic across session authority, frontend telemetry, analysis cache, and every document's open-handle set. Expiry removes entries whose deadline is less than or equal to the current second and leaves later deadlines intact. Generated session tokens are URL-safe unpadded encodings of 32 random bytes.
The standalone IDE resource contract is:
- one source apply must remain below 250 ms, and the repeated local in-process average below 40 ms;
- the HTTP apply path must remain below 250 ms maximum and 50 ms average in the focused reference fixture;
- reference p95 bounds are 2.5 s for boot-to-ready, 150 ms for completion, 150 ms for hover, 300 ms for diagnostics, and 400 ms for workspace search;
- the maximum completion request over the 2,000-line typing fixture is 800 ms; and
- content above the advertised maximum file size returns HTTP 413 or the
equivalent typed
too_largeerror before writing.
These are focused reference-environment budgets. They are regression gates for the shipped authoring path on the x86_64 CI reference environment, not universal real-time guarantees for arbitrary hardware or projects.
Unified Web IDE shell contract¶
The standalone Web IDE uses one unified product shell. /, /ui, /ide, and
supported tab deep links resolve to that shell, whose complete bundled module
set includes the workspace, editor, online, hardware, settings, observability,
logs, debug, and command surfaces. The shell preserves explicit tab panels,
status-bar state, compact toolbar and overflow actions, hidden-panel CSS
semantics, and ARIA-selected tab state.
Online control defaults to the same runtime web origin and normalizes both wrapped and direct API payloads. I/O configuration reads and writes follow the active workspace. The hardware surface preserves the MQTT connectivity probe, Runtime Cloud topology, and realtime-link transport projection; settings preserve the documented realtime-link fields. Removed legacy fleet mutation routes remain not found rather than forwarding to a current Runtime Cloud operation.
Unified Web IDE hardware and settings interaction contract¶
The Hardware and Settings tabs are rendered operator surfaces, not
source-presence claims. Their tab buttons use the tab/tabpanel ARIA
relationship, expose exactly one selected tab, update the /ide/<tab> route,
and preserve the selected tab across a same-origin reload. Ctrl+2 and
Ctrl+3 select Hardware and Settings when focus is outside a code editor.
Changing tabs does not itself write runtime.toml, io.toml, or
simulation.toml.
Hardware loads the active project's current runtime and I/O snapshots before
presenting them as configured state. A planned or offline runtime remains
visibly offline; the canvas, cards, or presence of configuration must not
invent a live connection. The component palette exposes its categories as
expandable buttons with accurate aria-expanded state. Hydrated I/O and
runtime communication drivers appear as labeled cards. Expanding and
collapsing the driver region updates both the visible region and the
controlling button's aria-expanded value.
Hardware-tab activation and route changes remain browser-responsive while the communication graph is laid out. Canvas relayout requests made before the next animation frame coalesce into one pending frame. Replacing or deactivating that request cancels both the pending frame and its delayed resize/fit timers. The latest request schedules one fit pass after 260 milliseconds, so project hydration cannot overlap or accumulate parallel resize/fit sequences.
Every driver-card configuration action carries one exact settings key and category. Activating it selects Settings, updates the route, renders the owning category, and focuses or exposes the corresponding field. Loopback and simulated drivers share the documented simulated-I/O settings rather than inventing a separate loopback schema. Runtime and endpoint actions preserve the selected runtime scope when routing to Settings. Merely opening a configuration action does not save a value.
Settings renders all categories plus a text filter. Filtering is case-insensitive over labels, keys, category names, and group text; it reports the visible and total field counts, hides nonmatching editable fields, and offers a clear action that restores the category. A no-match query displays an explicit empty result rather than stale fields. Category selection clears neither entered values nor the selected runtime. Credential-bearing password and token fields render with password input semantics. The Advanced category labels runtime state as read-only and keeps import, export, reset, and direct-TOML actions distinct from ordinary field edits.
Hardware and Settings use the shared ide-runtime-selection-changed event.
An accepted runtime-scope change on either surface is reflected by the other
without a write or feedback loop. Runtime-, I/O-, and simulation-backed fields
retain their distinct persistence targets. Structured JSON fields are parsed
and normalized before any write. Saves are serialized in user-action order,
use the current revision/version precondition, and dispatch the matching
runtime- or I/O-config-updated event only after a successful write. A parse,
authorization, conflict, transport, or backend failure is shown as a failure
and must not be reported as saved.
Automated acceptance must exercise these behaviors in the shipped browser surface. Module-source string checks may lock packaging details but do not prove rendering, interaction, routing, focus, accessibility state, or cross-tab synchronization. Final acceptance also retains a real rendered screenshot or recording for the Hardware and Settings journeys.
Standalone configuration UI workspace contract¶
The standalone configuration UI projects the current product mode, planned workspace runtimes, topology, Runtime Cloud configuration and rollout state, and the active runtime's I/O configuration from one selected workspace. Planned but disconnected runtimes remain visibly offline/degraded and their links failed; they are not reported as live because they exist on disk.
Runtime configuration writes use an expected source revision or digest. A stale
precondition returns conflict without changing the file; a current write
updates that runtime's runtime.toml. Structured Text writes remain inside the
selected runtime and the validation endpoint reports valid or invalid source
without treating invalid source as a successful validation.
Runtime create and delete operate on one named workspace runtime and are reflected in subsequent project-state and lifecycle queries. Lifecycle status identifies the workspace runtime and whether it is managed. Live-target profiles may be listed, upserted, selected, connected, queried, and removed; an unreachable target remains disconnected with its last error and active target visible.
Workspace runtime IDs are trimmed, nonempty, lowercased ASCII identifiers
containing only letters, digits, -, and _. Resolution is exact after
normalization; duplicate configured resource names reject the workspace.
Workspace discovery admits runtime.toml at the selected root and in direct
child directories only, sorts runtimes by normalized ID, and rejects an empty
workspace. A generated runtime contains validated runtime.toml, io.toml,
and src/main.st files beneath a new child named by that ID. Creation never
reuses an existing ID or filesystem path. Deletion cannot remove the last
runtime, the workspace root, or any path outside the workspace root.
Host-group input is trimmed and lowercased. ASCII letters, digits, -, and _
are preserved; every other character becomes -; an empty result is absent.
The generated runtime template uses default-host when no nonempty group is
supplied and binds its resource, service, Unix control socket, local web
listener, local discovery, peer mesh, and Dev Runtime Cloud identity to the
normalized runtime.
Configuration revisions are lowercase SHA-256 over the exact UTF-8 file text.
An expected revision is checked before validation or replacement. A mismatch
returns the current revision as a conflict and leaves the file unchanged.
Accepted writes validate the complete candidate and replace the destination
through a sibling temporary file. Structured Text paths are relative to
src/, remain contained there, and end in .st case-insensitively; absolute
paths, parent traversal, empty paths, and other extensions reject.
Live targets are trimmed HTTP(S) origins. A missing scheme receives http://;
an explicit scheme is lowercase http:// or https://. The target contains a
nonempty authority and no user-info, path, query, or fragment; one optional
terminal slash is removed. Profiles are keyed by normalized target and listed
in key order. A blank label defaults to the normalized target. Snapshots expose
the target and label, connection state, active target, last error, and update
time, but never the stored credential token. Removing the active profile also
clears its token, connection state, last error, and cached Runtime Cloud state.
Lifecycle probes accept configured tcp://<socket-address> and, on Unix,
nonempty unix://<path> endpoints. Empty, malformed, unsupported, or
unreachable endpoints are offline. Status distinguishes an externally running
runtime from a child managed by this configuration UI; stop and restart do not
claim ownership of or terminate an externally managed process.
Every configuration-UI response is JSON with the requested HTTP status.
Structured errors retain the stable error_code, message, ordered field-error
list, and optional conflict version without converting an error into ok:
true.
6.8.2 Versioned deployment and rollback¶
trust-runtime deploy validates the source bundle before installation and the
copied bundle before changing deployment pointers. A custom deployment label
must be one non-empty filesystem component: absolute paths, ./.., path
separators, and platform prefixes are rejected. Automatically generated labels
must be unique across consecutive deployments from one process, including
deployments started within the same second.
When deployment signature policy is enabled, validation occurs before any
bundle file or deployment pointer is changed. A payload signed by a currently
trusted, unexpired key is accepted. A tampered payload, unknown key, expired
key, malformed signature, or policy mismatch rejects the deployment without a
partial install. Failure output may identify the key ID and failure class but
must never echo key material or another configured secret. Source paths with an
optional leading src/ normalize to the same contained bundle destination;
normalization never permits an absolute path, parent traversal, or platform
prefix.
The deployment root contains bundles/, current, and optionally previous.
The pointer targets are resolved relative to the link location when an older
relative link is read; newly written links target the canonical absolute bundle
directory so a relative --root cannot create a broken pointer. Replacing a
pointer must also replace a dangling symlink instead of treating it as absent.
An ordinary file or directory at a pointer path is an error and is not deleted
or overwritten.
An existing pointer is eligible for retention or rollback only when it resolves
to a direct child directory of that deployment root's bundles/; a pointer to
another filesystem location is rejected rather than adopted.
Both deployment pointer slots are inspected before a new bundle is installed.
If either slot is occupied by an ordinary file or directory, deployment fails
without installing the candidate, changing either pointer, or publishing its
change summary.
Enabled ADS and OPC UA client configuration is self-contained in the installed bundle. Each configured sidecar path must be a normalized relative path contained beneath the source bundle, and the file is copied to the same relative path in the installed bundle. Absolute paths, parent traversal, platform prefixes, and symbolic links in any copied bundle path are rejected; deployment never follows a symbolic link while constructing the installed bundle.
After a successful deployment, current identifies the newly validated bundle
and previous identifies the bundle that was current immediately before the
deployment. Pruning retains exactly those existing bundle directories and
removes older directories only from the deployment root's own bundles/
directory. It must not delete the new rollback target. rollback swaps the two
valid pointers without copying or rebuilding either bundle.
Before deployment pointers change, the command constructs a deterministic
change summary that identifies both the bundle being deployed and the previous
bundle, when one exists. It publishes that summary only for a deployment whose
pointer transition succeeds. The runtime.toml and io.toml status is based
on the complete file content, so a change outside the selected
operator-friendly detail bullets must still be reported as an update rather
than as unchanged. Safe-state comparison uses the structured I/O address and
value, not a potentially lossy display form. Source comparison recursively
covers .st and .pou files case-insensitively beneath src/, with stable
relative-path ordering. A source directory traversal or file-read failure
aborts summary construction and the deployment; it is not converted into an
empty source set.
deployments/<label>.txt and deployments/last.txt contain the last successful
deploy command's change summary. They are not deployment pointers and rollback
does not rewrite them. The current symlink is the authoritative active-bundle
identifier before and after rollback.
6.8.3 Managed local fleet scaffolding¶
trust-runtime fleet runtime add creates one managed local runtime project
beneath the selected fleet root and registers it in fleet.toml. Runtime names
use the CLI's portable name alphabet and must be unique in a manifest; hand-edited
manifests are subject to the same validation before list or lifecycle actions.
Generated entries use the portable runtime name as a relative project path
beneath the fleet root. A hand-edited nonempty project path is resolved against
the fleet root when relative and retained as a local path when absolute; this
path flexibility does not relax name, endpoint, or port validation.
Every control endpoint must parse under the runtime control-endpoint contract,
every web port must be nonzero, and TCP control ports and web ports must be
unique across roles and runtime entries. Ambiguous or malformed manifests fail
before filesystem or lifecycle mutation.
Generated control and web listeners use distinct available loopback ports. The generated control credential is 32 bytes from the operating-system secure random source and is encoded without padding. If secure randomness is unavailable, scaffolding fails; it must not substitute timestamps, process IDs, zero-filled bytes, or another predictable credential. Resource names are derived deterministically from the portable runtime name, with a nonnumeric fallback/prefix. Generated runtime and I/O configuration is parsed and validated before the manifest is updated, and the source scaffold is emitted deterministically.
Fleet lifecycle status is control-endpoint evidence, not PID-file inference.
Failure to connect means stopped; once an endpoint accepts a connection, an
authentication rejection, malformed envelope, oversized response, mismatched
response ID, or missing Boolean ok is a command error and must not be
flattened to stopped. Failure to read or parse the managed project's control
credential is likewise an error. The PID file is advisory process metadata.
A successful shutdown request prepares and writes its complete correlated
control response before it signals the resource stop. The stop signal remains
part of the accepted request and is applied immediately after the response
write attempt; process teardown must not race the operator-visible
acknowledgement off the control connection.
status and logs are read-only and do not create .trust-runtime state when
it is absent. Log tailing retains only the requested final lines in memory
rather than loading the complete log file. A start operation may create its
lifecycle directory and open the child log as spawn setup, but it publishes the
advisory PID only after spawning the process. If PID persistence fails, it
terminates and reaps that child, and if the child exits before the control
endpoint becomes ready, start fails and removes the stale PID file.
Managed-fleet scaffolding always emits a loopback TCP control endpoint because
the manifest and lifecycle commands need one portable explicit endpoint. This
is distinct from the generic runtime default, which uses unix:// on Unix-like
platforms; a generated Unix socket is created with restrictive permissions
(0600) to prevent accidental exposure.
Managed-fleet projections are operator-facing and secret-free. ADS allowed
clients are rendered as human-readable identity/source summaries rather than
editable raw pin objects. Host names are trimmed, lose a trailing DNS dot, and
prefer the operating-system host name; a blank source is ignored and the stable
literal local-host is used only when no host authority exists.
6.9 Debug Attach (Production)¶
Attach debugging is optional in production deployments but must be supported by the runtime when enabled:
- Attach must not restart or reload the runtime.
- Attach must observe the current state (running/paused/faulted).
- Detach must not alter runtime execution.
- Debug hooks must be side-effect-free when disabled.
- Attach is gated by
runtime.control.debug_enabled. When disabled, debug control requests are rejected. The default is disabled in production mode (seeruntime.control.mode). runtime.control.modedefaults toproductionand can be set todebugfor development workflows;runtime.control.debug_enabledoverrides the mode when explicitly set.
6.9.1 Managed debug floating-point ingress¶
In a managed local debug session, stIoWrite and stIoForce interpret values
for declared REAL and LREAL addresses as semantic decimal values. They
reject NaN, positive infinity, negative infinity, overflow to a non-finite
declared value, and raw integer encodings of non-finite IEEE bit patterns
before queueing, forcing, or changing the process image. A rejected request
returns a failed DAP response and preserves both the previous process-image
value and the previous force state. Attach-mode requests remain governed by
the separate runtime control endpoint contract.
6.9.2 Debug mutation lifecycle¶
Debug writes are one-shot requests. An accepted write is applied at the next scan boundary and is then consumed. A force is applied at scan boundaries until the matching release request or a clearing lifecycle boundary. Release removes the force without writing a replacement value; normal program and I/O evaluation determine the value at the next scan.
Pause and resume preserve queued writes and active forces because neither operation creates a new runtime lifecycle. A non-terminating debugger detach or transport disconnect also preserves them: detach must not alter runtime execution, and a later authorized client must observe the existing force state. An authorization change governs subsequent write, force, and release requests; it does not silently mutate an already accepted force.
Deliberate resource stop, fault handling, and every warm or cold restart are clearing boundaries. Before safe-state handling or restarted execution can proceed, the runtime clears queued debug writes and active variable and I/O forces. Safe-state output therefore has precedence over debug forcing, and a request accepted before a clearing boundary cannot be replayed into the next runtime lifecycle.
6.9.3 Simulation coupling thresholds¶
The file-backed simulation.toml loader rejects NaN and positive or negative
infinity in an optional [[couplings]].threshold before returning or
activating the configuration. It does not normalize, clamp, or substitute a
threshold. This contract does not govern programmatic coupling-rule
construction or source-value admission.
6.9.4 Simulation model execution¶
An explicit [simulation].enabled value controls activation. When that field
is omitted, the loader enables simulation if the file contains at least one
coupling or disturbance, or enabled physics with at least one joint. An
explicit enabled = false remains authoritative even when model entries are
present. The loader preserves the configured seed and positive time scale,
sorts file-backed disturbances by due time, and preserves the declared
coupling and physics model. The seed is part of replay identity; changing it is
not required to alter a model that contains no stochastic element.
Post-cycle simulation observes the completed output image. A changed coupling
value is queued for its configured delay, with source >= threshold selecting
on_true and a lower value selecting on_false. Physics also queues encoder
feedback after stepping. Neither path writes the input image during
post-cycle processing. Pre-cycle processing applies due effects in due-time
then insertion order, so a zero-delay post-cycle result first becomes visible
at the following pre-cycle boundary.
A scripted fault disturbance in an accepted file-backed or programmatically
constructed model does nothing before its due time. At or after that time it
faults the runtime and returns a visible simulation error.
Accelerating the scheduler clock does not reinterpret scaled model time as
active watchdog execution time. An explicitly stopped, non-overrunning
accelerated resource reaches Stopped without a terminal error; genuine active
execution overruns remain governed by the separate watchdog contract.
The in-tree physics backend accepts revolute joints with output-bit enable sources and input-word feedback targets. A physics feedback target that duplicates a coupling target or an earlier physics target is rejected before activation. Each enabled joint advances by its fixed step, clamps to its configured limits, and publishes the rounded product of clamped angle and counts-per-radian, bounded to the unsigned 16-bit range, through the queued scan-boundary rule. A newly accepted joint starts at angle zero before its first fixed step. Replaying the same accepted model and inputs produces the same coupling and physics traces. Helper-selected trace length and a serialized trace hash are not part of product compatibility. This contract does not yet define different-seed behavior, programmatic disturbance reordering, or overflow policy beyond the currently specified finite configuration limits.
6.9.4.1 Simulation configuration and conversion boundaries¶
An absent optional simulation file yields no configuration; a present
unreadable or malformed file returns a path-qualified configuration error.
With no sections, file-backed defaults are disabled, seed zero, and time scale
one. File-backed time scale must be at least one. SimulationController
defensively treats a programmatically supplied scale of zero as one without
silently enabling a disabled model.
Coupling sources are concrete %Q addresses and targets are concrete %I
addresses. Thresholds are finite. on_true and on_false require a threshold,
are parsed according to the target process-image width, and must fit that width
without truncation or wrapping. Coupling delays must fit the signed runtime
duration range. Without a threshold, numeric coupling conversion to
BYTE/WORD/DWORD is checked and overflow is an error; LWORD preserves the
accepted unsigned value. Bit conversion uses zero/nonzero truth. Fixed byte
strings must fit their configured byte count.
File-backed set disturbances default to kind set, require a concrete %I
target and a width-valid value, and require at_ms to fit the signed runtime
duration range. Fault kind is ASCII-case-insensitive and defaults its message
when omitted. Unknown kinds and missing set fields are rejected. Disturbances
are stably sorted by due time: declarations with equal due times retain file
order.
Physics configuration defaults to the in-tree Rapier backend, a 10 ms fixed
step, and 1000 encoder counts per radian. The step is within
1..=floor(i64::MAX / 1_000_000) milliseconds, the largest millisecond value
representable by the signed nanosecond runtime duration. Global and per-joint
counts-per-radian are finite and positive. Revolute joint velocity and bounds
are finite, lower does not exceed upper, IDs are unique, enable sources are
%QX bits, and feedback targets are %IW words. Disabled physics or enabled
physics with no joints does not infer simulation activation and creates no
physics controller.
Disabled controllers perform no disturbance, coupling, or physics I/O. Enabled pre-cycle processing applies equal-time disturbances and effects in stable insertion order. A source value that has not changed since its last post-cycle observation does not enqueue another effect. Backend address errors while reading a coupling remain I/O errors; a failed injected or delayed input write and a physics enable-read failure become visible simulation faults. These checks prove deterministic in-process model semantics, not physical plant fidelity.
6.9.5 Runtime control authorization¶
Runtime control uses the ordered role hierarchy viewer < operator < engineer <
admin; a higher role includes lower-role permissions. Authorization is checked
before dispatch, so a denied request must not change runtime, debug, I/O, HMI,
configuration, pairing, or connector state.
A role denial returns the stable wire error code insufficient_role together
with the required role in the human-readable error. Missing and invalid
credentials retain their separate authentication error codes. Clients must use
the stable code, not parse the prose, when distinguishing authentication from
authorization failure.
The control endpoint grammar is exact and whitespace-sensitive. tcp:// is
followed by a numeric IPv4 or bracketed IPv6 socket address with a required
port, and only a loopback IP is admitted. Hostnames, non-loopback addresses,
missing ports, trailing text, case variants, and leading/trailing whitespace
reject before binding. On Unix, unix:// is followed by a nonempty socket
path; no implicit default path is created. Endpoint accessors preserve the
parsed address/path, and a started server owns the same shared control state
supplied by its caller.
Control source identity preserves registration order, IDs, paths, and text.
Exact stored paths have first priority. For a relative request with a project
root, lookup next considers <root>/<request> and, for a single path
component, <root>/src/<request>. Existing candidates may match an existing
registered path through filesystem canonicalization. Only after those checks
may a relative suffix match be used, and it succeeds only when exactly one
registered path has that suffix. Ambiguous suffixes and unmatched absolute
paths return no file. Source-text lookup is exact by file ID and returns the
first registered row for a duplicate ID.
Web-to-control dispatch injects a request token only when the JSON payload has
no auth member; an explicit string, null, or malformed auth member is never
overwritten. Communication operations comm.apply, comm.test,
comm.browse_symbols, and ads.import_symbols.apply receive one
server-owned credential_channel parameter. Missing client identity, unix,
loopback, and numeric loopback socket addresses classify as
trusted_same_host; every other client identity classifies as
untrusted_remote_plain_tcp. The server-owned value replaces a caller-supplied
value, but a non-object parameter shape remains unchanged for the handler to
reject. Other operations receive no injected field.
Successful status responses whose result is an object include access
capabilities for the authenticated role. Viewer and Operator cannot write,
force, release, or write HMI values; Engineer and Admin can. A denied role
includes a stable human-facing reason beginning with the capitalized role,
while Engineer and Admin use JSON null. Non-object or absent results are not
rewritten.
Authentication and transport defaults are:
- A configured control token identifies the Admin role. A valid pairing token identifies its stored role. Missing and invalid tokens fail before role authorization whenever control authentication is required.
- An untrusted network client without configured authentication receives only Viewer authority. A local Unix control socket with no configured token may use Admin authority because the socket is local and created with mode 0600.
- An unsupported request type performs no handler action. A request type that is not explicitly classified by the role policy uses an Admin fail-safe until it is reviewed; it must never inherit Viewer authority by default.
The explicit operation matrix is:
| Minimum role | Operations |
|---|---|
| Viewer | status, health, tasks.stats, events.tail, events, faults, config.get, connectors.status, ADS/discovery/status/route-plan reads, comm.capabilities, comm.schema, comm.discover, fleet.topology, io.list, io.read, HMI schema/value/trend/alarm/descriptor reads, historian reads, debug state/stack/scope/variable/location reads, breakpoints.list, var.forced |
| Operator | pause, resume, restart, hmi.alarm.ack, pair.claim |
| Engineer | step operations, breakpoint mutation, eval, set, variable and I/O write/force/release, debug.evaluate, HMI write/descriptor/scaffold mutation, communication apply/test, ADS server doctor actions |
| Admin | shutdown, bytecode.reload, ADS route add/remove, pairing start/list/revoke, and security/exposure configuration listed below |
Parameter-sensitive operations retain these boundaries:
- ADS doctor and symbol import/browse are Viewer-only for cached/offline reads
and Engineer for live-device or write-enabled work. Presence of a non-null
live
targetrequires Engineer even if a snapshot is also present; later request validation may still reject that ambiguous combination.ads.import_symbols.applyis always write-enabled and therefore always requires Engineer, including when its source symbols came from a cached snapshot. config.setrequires Admin if any request key iscontrol.auth_token,control.mode,control.debug_enabled,web.auth,mesh.auth_token,mesh.role,mesh.zenohd_version,mesh.plugin_versions,runtime_cloud.profile,runtime_cloud.wan.allow_write, orruntime_cloud.links.transports. Other currently supported configuration keys require Engineer.- Debug variable and I/O write, force, and release operations require Engineer for every address class. Viewer and Operator requests are denied before any queued write or force state changes. Enabling the debug surface itself requires Admin; an Engineer may use an already enabled surface but cannot enable it.
The registry is the single classification authority. Its operation names are exact, lowercase wire tokens; case variants, whitespace variants, and unknown names are unclassified and receive the Admin fail-safe. Every dispatchable operation appears exactly once, no non-dispatchable alias receives authority, and the debug-surface flag is true only for debug state, stack, scope, variables, locations, pause/resume/step, breakpoint, evaluation, variable write, variable force, and variable release operations. A classification change is a security-contract change and requires the complete role matrix to be reviewed, not merely one example request.
6.9.6 Control credentials, pairing, and TLS materials¶
Role text is trimmed and matched ASCII-case-insensitively against exactly
viewer, operator, engineer, and admin; serialization and display use
those lowercase spellings. Permission inclusion follows that exact total
order. Secret comparison returns true only for equal bytes and equal length,
including the empty string, and examines the complete expected byte length
even when the provided value is shorter.
Control authentication applies this precedence:
- A configured primary control token, when exactly matched, grants Admin.
- Otherwise a supplied valid pairing token grants its stored role.
- If credentials are required, an absent token returns
missing_auth_tokenand a supplied but unmatched token returnsinvalid_auth_token. - Without required credentials, a client explicitly identified as
unixor an in-process client with no transport identifier receives Admin. A network-style client identifier containing:receives Viewer. An opaque non-network identifier does not by itself reduce local authority.
Credential-state lock failure is authentication failure. It cannot be interpreted as “no token configured” and cannot grant local or network authority.
A pairing session owns at most one pending code. Starting again replaces the
previous pending code. A code is exactly six ASCII decimal digits, expires
300 seconds after creation, and remains valid at its exact expires_at
second. Claim input trims surrounding whitespace. A wrong code does not
consume the pending session; an expired or successful claim does. A successful
claim returns one URL-safe, unpadded 32-byte random token, stores it enabled,
and assigns a unique stable ID even when multiple claims occur in one clock
second.
The default pairing role is Operator. Viewer, Operator, and Engineer requests
retain their requested role; an Admin request is reduced to Engineer so the
pairing flow cannot mint an Admin credential. At most 256 enabled pairing
tokens may exist. Reaching that bound rejects the claim without creating a
token, and a later claim is possible after a token is revoked. Tokens expire
30 days after creation and remain valid through their exact expires_at
second. Validation accepts only an enabled, unexpired, byte-exact token and
returns its stored role.
Listing preserves storage order, never exposes the token, and reports only a Unicode ellipsis plus the last at most four characters. Revoking an exact ID disables only that token and is idempotently false when the ID is absent. Revoke-all returns the number that changed from enabled to disabled. Disabled tokens remain visible until expiry but never validate and are not counted toward the enabled-token limit.
The pairing file is a JSON object with a tokens array. An absent file loads
an empty store. Unreadable or malformed content grants no token authority.
Legacy rows with expires_at = 0 derive
created_at + 30 days with saturating arithmetic; already expired legacy rows
are pruned rather than temporarily revived. Expired rows are removed on load
and before every pairing, claim, validation, list, or revoke operation.
Successful token creation and revocation become externally successful only
after the resulting file is durably written. On Unix the credential file mode
is 0600; replacement must not expose a partial JSON credential set.
TLS material loading is inert when TLS mode is disabled: paths may be absent and no file is read. Every enabled mode requires certificate and private-key paths. An absolute path is used unchanged; a relative path requires a project root and resolves against it. An optional CA path resolves by the same rule. Read failures identify certificate, key, or CA ownership and the resolved path. When CA is omitted, the server certificate bytes are the client trust root. The tiny-http projection contains owned copies of certificate and private-key PEM only.
A rustls server configuration requires at least one valid certificate and one valid private key, and rejects malformed, empty, or certificate/key-mismatched material. A rustls client configuration requires at least one valid CA certificate and rejects malformed or empty trust roots. Provider installation is process-wide and repeatable; failure remains a visible control error rather than silently selecting an insecure configuration.
6.9.7 Runtime web authentication and POST admission¶
The embedded web surface applies one shared authentication contract before a
route may read or mutate runtime state. In local mode, authority is derived
only from the request socket: an IPv4 or IPv6 loopback peer receives Admin and
every non-loopback or unavailable peer identity is unauthenticated. In token
mode, the first X-Trust-Token header name is matched
ASCII-case-insensitively, while its value is byte-exact and is not trimmed. An
exact primary web token grants Admin. Otherwise, an exact enabled pairing token
grants its stored role. Missing, unknown, expired, or disabled tokens are
unauthenticated. Failure to read primary credential state fails closed and
must not fall through to pairing authority.
Role authorization follows the hierarchy in section 6.9.5. An authenticated
role below the route requirement returns forbidden; absent authority returns
unauthorized. Both denials are JSON objects with ok: false and the stable
error text, use application/json, and return HTTP 403 and 401 respectively.
The accepted token value is forwarded to control dispatch. A server-owned
internal control token is used only when the web request supplied no token; it
must not replace an explicit token. The X-Trust-Ide-Session header name is
also ASCII-case-insensitive and its value is preserved exactly.
Every state-changing API route applies the following POST admission rules before parsing or dispatch:
- Where JSON is required,
Content-Typeis the media typeapplication/json, matched ASCII-case-insensitively and optionally followed by media-type parameters. Prefix and suffix lookalikes such asapplication/jsonxandapplication/json-patch+jsonare rejected with HTTP 415 andcontract_violation. - A request without
Originis accepted only when its socket peer is loopback. A non-loopback or unavailable peer withoutOriginis rejected with HTTP 403 andpermission_denied. - A supplied Origin requires Host. The literal opaque origin
nullis denied. Otherwise, after ASCII case normalization and removal of at most one terminal slash, Origin must equal<scheme>://<host>, where the scheme ishttpsexactly when web TLS is enabled and Host is the trimmed request Host value. Host, scheme, port, path, user-info, and prefix/suffix mismatches are denied; origin comparison never uses prefix matching. - JSON body admission is inclusive at the configured byte limit. Reading at
most one byte beyond that limit distinguishes an oversized body from a
malformed JSON body without unbounded allocation. A body of exactly the
limit is parsed; a larger body returns HTTP 413. Empty or malformed JSON
returns HTTP 400. Limit arithmetic is overflow-safe for every
usizevalue.
POST-policy denials are JSON with Content-Type: application/json, ok:
false, a stable denial_code, and the documented error text. Admission proves
only request framing, origin, and body shape; route authentication,
authorization, and semantic validation remain separate mandatory checks.
Shared web helper grammar is deterministic:
- Header names are ASCII-case-insensitive; the first matching value is trimmed at its outer ASCII whitespace and otherwise preserved.
- A web listen socket projects to
httporhttpsaccording to TLS. Wildcard IPv4 projects tolocalhost; hostnames and IPv4 retain their host and port; IPv6 is emitted in bracketed URL authority form. - Query parsing uses the first exact key.
limitis an unsigned decimal integer. General values decode+as space and valid%HHoctets before lossy UTF-8 conversion. A malformed or incomplete percent escape is preserved literally rather than fabricating a NUL byte or silently deleting source characters. - A Runtime Cloud rollout action path is exactly
/api/runtime-cloud/rollouts/<nonempty-id>/<nonempty-action>with no extra path component. Outer whitespace inside either captured component is removed; path-prefix, suffix, and case variants do not match. - Probe JSON succeeds only for
ok: true. Success projectsresult.plc_name, falling back toresult.resourceand thenPLC, plusresult.statefalling back toonline. Invalid JSON, missing/falseok, or a non-string error projects{ok:false,error:"unreachable"}; a string error is preserved. - QR output is a complete SVG for the exact input bytes or a visible control error when the payload exceeds encoder capacity. Wall-clock helpers expose Unix-epoch milliseconds and nanoseconds, with nanoseconds never earlier than the contemporaneous millisecond value multiplied by one million.
6.10 Configuration and Resources¶
IEC configurations model one or more resources (IEC 61131-3 Ed.3, §6.8.1;
Table 62). The runtime scheduler supports several independently constructed
resources, each scheduled in its own OS thread. The current
CompileSession::build_runtime source profile constructs exactly one resource:
it accepts zero or one explicit RESOURCE and rejects a multi-resource source
configuration instead of flattening it. Multi-resource orchestration therefore
uses separately constructed runtimes; one source build does not yet project an
IEC multi-resource configuration into several runtime objects.
Cross-resource data exchange is limited to explicitly declared globals (e.g., VAR_GLOBAL in configuration scope). (IEC 61131-3 Ed.3, §6.8.1; Table 62) Shared globals are synchronized under a single configuration lock: each resource cycle copies shared values in, executes ready tasks, then writes back updates before releasing the lock. This preserves deterministic ordering while serializing shared-global access.
Runtime configuration loading and validation contract¶
runtime.toml, io.toml, and enabled connector sidecars are truST product
contracts, not IEC decisions or deviations. Their user-facing field reference
is maintained under docs/public/reference/config/; the following rules are
normative for parsing and activation:
- Runtime and I/O TOML use closed schemas. Unknown fields, wrong shapes,
unsupported enum values, incomplete conditional groups, and invalid security
combinations fail as
InvalidConfigbefore a configuration is returned. Validation errors retain the owning file name. Defaults are applied only to omitted documented optional fields and never replace a malformed explicit endpoint, path, interface, producer path, symbol pattern, or version-map entry. When any of those values is present, it must be nonempty after trimming; list and map parsing must not silently discard blank entries. - Resource, task, retain, watchdog, ADS worker, OPC UA client polling, and
other millisecond values converted into the signed runtime
Durationmust be representable without narrowing. The runtime duration stores signed nanoseconds, so unless a field has a stronger documented minimum, its accepted millisecond range is1..=9223372036854(i64::MAX / 1_000_000); larger values reject instead of overflowing or panicking during conversion. OPC UA client polling retains its stronger minimum of 10 milliseconds. io.tomlaccepts exactly one nonempty driver form:io.driverwith table parameters, or a nonemptyio.driverslist whose names are nonempty and whose parameters are tables. Safe-state values are parsed against their addressed output width. BYTE, WORD, DWORD, and LWORD accept only their exact unsigned ranges; narrowing, wrapping, clamping, or substitution is forbidden.- TCP control requires a nonempty authentication token. Enabled remote web or mesh surfaces obey the configured TLS requirement. Enabled TLS requires the documented certificate/key group, provisioned TLS additionally requires a CA, signed deployment requires a keyring, and enabled connector/server sections must satisfy their documented credential, endpoint, allowlist, capacity, and explicit insecure-transport acknowledgement rules.
- Bundle loading requires a project directory,
runtime.toml, validated project or system I/O configuration, andprogram.stbc. ADS and OPC UA client sidecars are loaded only when enabled; relative sidecar paths resolve against the bundle root, enabled missing sidecars fail visibly, and the retained evidence hash covers the exact bytes that were parsed. - Loading is fail closed. No partially parsed runtime, I/O, connector sidecar, or default substitute is returned after any required file read or validation failure.
The configuration contract enums use trimmed ASCII-case-insensitive input and
closed vocabularies. Web authentication accepts local or token. TLS accepts
disabled, self-managed (with the compatibility spelling self_managed),
or provisioned. Mesh role accepts peer, client, or router. Runtime-cloud
profile accepts dev, plant, or wan; Plant and WAN require secure
transport while Dev does not. Preferred runtime-cloud transport accepts
realtime, zenoh, mesh, mqtt, modbus-tcp (with compatibility spelling
modbus_tcp), opcua, discovery, or web. OpenOT fence mode accepts
fenced or unfenced, and its source accepts heartbeat or exact st-fb.
Canonical output always uses the hyphenated lowercase spelling. Empty text,
near matches, extra words, unsupported punctuation, and NUL-bearing values
reject with the owning configuration key in the error.
Default optional runtime connectors are disabled. The ADS client sidecar is
ads.toml with a 20-millisecond worker tick; the OPC UA client sidecar is
opcua_client.toml with a 250-millisecond polling interval. Default OpenOT
telemetry is disabled, has an empty path, capacity 4096, fenced mode,
heartbeat source, no producer alias or producer list, and does not allow
unfenced proof. These defaults apply only when the corresponding documented
configuration is omitted; they cannot replace malformed explicit values.
Runtime web setup and I/O projection contract¶
The web setup surface uses the explicitly selected bundle root, otherwise the
current directory. A derived default resource name is the final path component
with every non-ASCII-alphanumeric character replaced by _; a missing
component uses trust-plc. Existing valid runtime.toml and project
io.toml values take precedence over platform defaults. Setup need remains
true while runtime configuration is absent or neither project nor system I/O
configuration exists.
I/O mutation accepts either a nonempty ordered drivers list or the legacy
single driver, with the list taking precedence when present. Driver names are
trimmed and nonempty. Parameters are JSON objects converted recursively to
TOML tables; null becomes an empty string, and Boolean, representable integer,
float, string, array, and object shapes retain their value. A top-level
non-object parameter rejects rather than being wrapped or discarded. Every
driver is enabled in the resulting configuration.
The I/O response preserves driver order, parameters, safe-state order, source
identity, and system-I/O selection. Its legacy primary driver and params
are the first configured driver. Address projection uses %I, %Q, or %M
plus X, B, W, D, or L, retains bit indices, and emits %<area>* for a
wildcard. Boolean safe values use TRUE/FALSE; integer and bit-string widths
use unsigned or signed decimal without narrowing.
Saving project I/O validates the complete rendered TOML before replacement.
Selecting system I/O removes only the project io.toml; selecting project I/O
does not change system configuration. Setup validates and writes
runtime.toml, writes validated project io.toml unless system I/O is
selected, and invokes system setup only when explicitly requested.
Source listing inspects the direct src/ directory, includes regular .st
files case-insensitively, and sorts names. Source reads canonicalize both the
src/ root and requested path and reject traversal or symbolic escape. HMI
asset reads apply the same containment rule under hmi/ and admit only a
regular file with lowercase .svg; another extension, directory, traversal,
or symbolic escape rejects.
6.11 Bytecode Format (Overview)¶
The executor consumes a stable bytecode container format emitted by the compiler. See the "ST Bytecode Format Specification" section in this document for details. - Instruction encoding and versioning - Program/function/function-block layouts - Constant pools and type descriptors - Resource, task, and POU metadata required by the runtime (process image sizing, task associations)
The runtime rejects unsupported major bytecode versions before configuring resources.
6.12 Browser UI, Discovery, and Mesh (Operational UX)¶
Operational UX is browser‑first (no app). A built‑in web service exposes an operational UI and discovery metadata. This is implementer‑specific and outside IEC 61131‑3 scope.
Configuration (in runtime.toml):
[runtime.web]
enabled = true
listen = "0.0.0.0:8080"
auth = "local" # local|token
[runtime.discovery]
enabled = true
service_name = "truST"
advertise = true
interfaces = ["eth0", "wlan0"]
[runtime.mesh]
enabled = false
listen = "0.0.0.0:5200"
auth_token = "change-me"
publish = ["Status.RunState", "Metrics.CycleMs", "TempA"]
[runtime.mesh.subscribe]
"Plant-1:TempA" = "RemoteTemp"
Rules:
- Local‑only by default. Remote access must be explicitly enabled.
- Request isolation is bounded. The runtime admits read-only requests on a
four-worker lane with at most 32 queued requests and body-bearing or mutating
requests on a separate four-worker lane with at most four admitted requests.
An incomplete request body must not prevent an unrelated admitted request
from being served.
- Saturation fails visibly. A request that cannot be admitted without
exceeding its lane's fixed bound receives HTTP 503 Service Unavailable, a
Retry-After: 1 header, and the stable denial code server_busy; the server
does not wait behind an incomplete body or create an unbounded worker thread.
These limits are internal runtime safety constants in this contract version,
not runtime.toml settings.
- Discovery uses mDNS/Bonjour on the local LAN only.
- The advertised and browsed service type is
_trust._plc._tcp.local.. Service construction enables automatic interface
addresses. The TXT projection preserves stable runtime id and name,
web port and TLS flag, optional mesh port, control endpoint, and a trimmed
nonempty host group; a missing or malformed optional value is absent rather
than fabricated.
- A resolved service replaces the entry with the same runtime ID and receives a
fresh observation timestamp. A removal event matches the exact entry ID or
runtime name, or an mDNS instance name whose final -<runtime-name> suffix
identifies that runtime. It removes only the matching entry.
- Remote access supports manual add and invite/QR pairing only.
- Data sharing is explicit (publish/subscribe mapping only).
Realtime T0 transport and cycle contract¶
Realtime T0 is a truST host-runtime transport outside IEC 61131-3. It is separate from the generic network mesh described below.
Route and QoS admission
QosTier::T0HardRtadmits onlyRealtimeRoute::T0HardRt.QosTier::T1Fast,QosTier::T2Ops, andQosTier::T3Diagadmit onlyRealtimeRoute::MeshIp.- A T0 publisher or subscriber bind requested through
MeshIpreturnsT0ErrorCode::ContractViolation. No mesh/IP connection is attempted.fallback_denied_totaland the selected channel'sfallback_denied_counteach increase by one for the denied bind. The diagnostic contains the stable fragmentnon-HardRT. - The canonical T0-to-communications error mapping is:
| T0 error | Communications error |
|---|---|
NotConfigured |
NotConfigured |
ContractViolation |
RtContractViolation |
SchemaMismatch |
SchemaMismatch |
StaleData |
StaleData |
TransportFailure |
TransportFailure |
The stable remediation for RtContractViolation is:
Use pre-bound T0 handles and fixed-layout payloads; generic IP mesh is non-HardRT.
Registration, shared-memory handshake, and binding
- Channel registration validates a nonempty channel identity, schema identity, and schema hash plus a positive fixed slot size and positive bounded-spin limits before publishing readiness.
- Failure to create or open the configured shared-memory root returns
T0ErrorCode::TransportFailure; its diagnostic identifies T0 shared-memory directory provisioning with the stable fragmentfailed to create T0 SHM directory. - When page pinning is configured as required, an unavailable pinning provider
or failed pinning operation returns
T0ErrorCode::TransportFailure; the diagnostic contains the stable fragmentrequired page pinning failed. - Opening an existing channel mapping requires its channel identity, schema
identity, schema version, schema hash, slot size, stale threshold,
bounded-spin limits, and ownership to match the requested contract. A
mismatch returns
T0ErrorCode::TransportFailurewith the stable fragmentschema_hash contract mismatchbefore the runtime can use that mapping. - A same-length mutation of the reviewed shared-memory header is rejected on
the next registration as
T0ErrorCode::TransportFailure. The bounded deterministic corruption campaign does not authorize parsing arbitrary damaged metadata or changing the mapping length. - Publisher and subscriber binding requires a previously registered channel,
T0HardRt,fixed_layout = true, a positive payload size no greater than the registered slot, an exact schema-hash match, and initialized pinned channel state. An absent channel returnsNotConfigured; a schema mismatch returnsSchemaMismatch; a variable or invalid layout returnsContractViolation; and unready or unpinned state returnsTransportFailure. - The resulting publisher and subscriber handles retain the bound channel, route, payload size, and schema identity. Publish and read operations do not perform route discovery, key parsing, generic mesh calls, or substitute an unbound transport. The source-bound T0 hot-path gate keeps mesh/discovery and key-parsing APIs out of the reviewed shared-memory modules while retaining the explicit diagnostic that generic IP mesh is non-HardRT.
Latest payload, overrun, stale, and bounded-spin results
- A successful publish commits one payload whose length equals the bound
fixed-layout size. A later publish before the preceding value is read
replaces the readable payload with the latest value and increments the
channel's cumulative
overrun_count. - A successful fresh read copies that latest committed payload, returns its
monotonically increasing sequence and byte count, reports
dropped_updates = newest_write_sequence - previous_read_sequence - 1, and exposes the cumulativeoverrun_count. The read advances the channel's read sequence and resets its consecutive stale-miss count. - A read with no new committed payload returns
NoUpdatewhile the consecutive miss count remains belowmax(stale_after_reads, 1). Reaching that threshold returnsT0ErrorCode::StaleDataand incrementsstale_count. - A reader observing an unstable writer retries only within both configured
bounds:
max_spin_retriesandmax_spin_time_us. Exhausting either bound returnsStaleData, incrementsspin_exhausted_count, and incrementsstale_count. - Publish and read both require initialized pinned channel state. Either
operation returns
TransportFailureinstead of accessing an unready or unpinned mapping.
Cycle exchange and cloud-work isolation
begin_cycle(cycle)records the cycle identity, clears the current cycle's pre/post observations, and restoresmax_cloud_ops_per_cycleas that cycle's cloud-work budget. It does not clear the cumulative denied-work count.- Within a cycle,
PreTaskmay be marked at most once.PostTaskmay be marked at most once and only afterPreTask.PostTaskbeforePreTask, or a duplicate exchange point in the same cycle, returnsT0ErrorCode::ContractViolation. - A cloud-work request receives
min(requested_ops, cloud_budget_remaining). The remainder is denied, subtracted from no T0 budget, and added todenied_cloud_ops_total; granted work reduces only the current cycle's cloud budget. - Consequently, a budget of three grants two operations and then one from requests of two and three, recording two denied operations. A budget of one across ten separately begun cycles grants ten of ten requests for fifty and records 490 denied operations.
- Functional T0 publish/read ordering, payload sequences, and stale/overrun counters remain independent of excess cloud-work admission. This is an accounting and ordering contract, not a measured execution-time guarantee.
Cross-process visibility and proof ceiling
- Two processes using the same shared-memory root and an exact matching channel contract may bind the permitted publisher/subscriber roles. After a successful committed publish, the subscriber can observe the same payload and a fresh read result through the shared mapping.
- Focused software checks of route rejection, shared-memory exchange, functional sequences, and counters do not prove worst-case latency, cycle deadlines, scheduler jitter, PREEMPT_RT configuration, production memory locking, physical topology, or hard-real-time certification. Those claims require separately named platform and timing evidence.
Linux realtime host profile contract¶
The Linux realtime profile parser accepts fifo, rr, and other as the
documented scheduler identities. An enabled profile rejects scheduler other
and duplicate CPU-affinity entries before host activation. Kernel readiness
classification distinguishes an explicit PREEMPT_RT signal from an explicit
non-RT signal and retains an unknown result when neither signal is present.
The reviewed /proc parsers preserve the user-space realtime scheduler and
priority and read the requested memory-lock field without inventing a value.
When observed FIFO policy and priority match the request, the reviewed observation contains no errors. A strict profile hook returns an error and retains the collected error detail when affinity or another reviewed readiness check fails; it cannot report a failed profile as active. These parser and failure-path checks do not prove that the validation host is PREEMPT_RT, that page locking succeeded in production, or that any deadline or jitter bound was met.
Mesh envelope, mapping, and numeric codec contract¶
- A published mesh envelope carries the runtime source identity, an unsigned sequence, a publication timestamp, and the encoded PLC value. A successful encode/decode round trip preserves the source, sequence, and supported value.
- A subscription source uses
peer:key. Parsing splits at the first colon, trims both parts, and accepts the mapping only when both the peer and remote key remain nonempty. - Inbound mesh values are decoded against the current local target type. Integer
values outside that IEC type's range are rejected rather than wrapped, and a
JSON number targeting
REAL/LREALis accepted only when the resulting runtime value is finite. - A rejected mesh value queues no local update and leaves the current PLC value unchanged. The runtime does not clamp, wrap, normalize, or substitute a default value.
- The supported scalar codec is bounded and total over arbitrary input bytes: malformed bytes may be rejected, but decoding must not panic. This crash-only malformed-input guarantee does not claim a particular diagnostic or that every malformed byte sequence is rejected.
Mesh control-plane readiness, liveliness, QoS, and snapshot contract¶
- Mesh QoS classification uses slash-delimited key-space segments.
activetakes precedence overcfg, which takes precedence overdiag; a key with none of those segments uses the fast data profile. - The liveliness registry keeps a unique current peer set and a bounded insertion-ordered join/leave history. A leave removes only that peer and records the transition.
- Cloud readiness requires an established session, liveliness token/subscriber, and successfully declared identity and catalog queryables. A bounded wait whose deadline expires before all four conditions hold returns an error rather than readiness.
- Startup readiness flags for identity and catalog mean that the corresponding queryables were declared successfully. Those flags alone do not prove a query/reply payload exchange.
-
A resource snapshot response that times out or disconnects is an error. It must not be returned as a successful empty snapshot.
-
TOML remains the source of truth; offline edits are supported.
The local terminal dashboard is a projection of the same control contract. It parses resource status, tasks, I/O drivers, events, simulation state, and settings without inventing missing health. Settings include cycle interval and simulation fields. Command routing preserves the beginner guard and pause semantics; prompt navigation cannot mutate a read-only view. The rendered dashboard keeps its reviewed status, task, I/O, and event regions in a stable layout so an operator can compare snapshots without hidden field movement.
Runtime Cloud state, dispatch, and rollout contract¶
Runtime Cloud state projects the connected runtime context, discovery transport metadata, topology nodes and links, and their current lifecycle and health without inventing connectivity. A fresh mesh disconnect first becomes stale and only later partitioned according to the documented staleness threshold. Plant and WAN state access requires the secure profile transport preconditions. Removed device-list mutation routes return not found rather than changing topology through a legacy alias. Corrupt persisted configuration state becomes an explicit error with retained parse evidence and is never replaced by an in-sync default.
Dispatch evaluates the selected target before any operation. An unreachable target or exhausted bounded query budget returns a visible denial and must not fall back to applying the request on the local runtime. A local target remains operational when an unrelated peer is partitioned. A successful remote status, configuration, or I/O proxy request returns the remote payload. Configuration dispatch preserves one request correlation through the target control audit, and both successful and failed target operations retain their matching audit identity and semantic result.
A rollout advances through its ordered target states and completes only after the applied configuration is verified. An apply or reconciliation failure makes both the rollout and affected target fail with retained error evidence. Abort transitions an active rollout and its unfinished targets to aborted. Pause, resume, or abort against a terminal rollout returns conflict and cannot rewrite the terminal result.
Runtime Cloud action preflight and profile contract¶
Runtime Cloud federation is a truST product contract, not an IEC decision or
deviation. The /api/runtime-cloud/actions/preflight and
/api/runtime-cloud/actions/dispatch surfaces use the current 1.x contract;
minor-version changes within that major are additive, while another major or a
malformed version is a contract violation.
Every action request has a nonempty request_id, connected_via, actor, and
action_type, plus at least one nonempty target runtime. A target runtime may
occur only once in a request. connected_via must identify the local runtime.
Malformed global request state is a denied preflight even when no per-target
decision can be emitted; an empty decision list must never turn a contract
violation into allowed: true. Rejecting an empty, blank, or duplicate target
list prevents both a false successful no-op and repeated side effects against
one runtime.
Preflight is side-effect free and evaluates the complete request before
dispatch. status_read, cfg_apply, and cmd_invoke are the supported action
families; unknown actions are contract violations. Viewer may read status,
Operator may invoke ordinary commands, Engineer may apply ordinary
configuration, and the security/exposure keys listed in the runtime control
authorization matrix require Admin. Stale and unreachable nonlocal targets are
denied with stable per-target reasons. A dispatch whose dry_run flag is true
returns the preflight result without performing the mapped control operation.
Profile policy is applied after the common contract, role, freshness, and reachability checks:
devdoes not useruntime.cloud.wan.allow_write; cross-site writes remain governed by the ordinary role and target checks.plantrequires token-authenticated TLS and secure transport metadata for a nonlocal target, but it does not use the WAN write allowlist.wanhas the same secure transport preconditions and additionally denies every nonlocalcfg_applyorcmd_invokeunless an explicit action-and-targetruntime.cloud.wan.allow_writerule matches.*, a prefix such assite-b/*, a suffix such as*/runtime-b, and exact target IDs have their documented meanings; other embedded-wildcard shapes do not match.
The runtime communications conformance gate must execute every named native
case in its suite. A stale filter that selects zero tests is a gate failure and
must not be reported as conformance success. Case execution evidence is scoped
to the current invocation; a passing result retained in a reused output
directory cannot satisfy a later run. Its development-profile gateway case
verifies the permissive dev policy above; default-deny write coverage belongs
to the wan profile and must not be attributed to dev.
Preflight and dispatch responses retain the caller's request_id and
connected_via. Each dispatched target produces one result for that target;
successful target results carry the target audit correlation supplied by the
control operation. A denied or failed target must remain visibly denied or
failed and must not be summarized as a successful aggregate action.
A live update to runtime.cloud.wan.allow_write uses the ordinary validated
configuration-write path. The accepted exact action and target pattern is
visible in subsequent configuration reads, and the control audit retains the
configuration request correlation. An invalid policy update is rejected before
replacing the active allowlist.
The Runtime Cloud control and I/O proxy planners are action-request builders,
so they must either produce an action that already satisfies this common
contract or reject the proxy request before preflight and before any local or
remote side effect. Both planners validate API compatibility and reject a blank
actor, target runtime, or connected_via. A control proxy request may omit its
request ID, in which case the planner generates one; an explicitly supplied
blank request ID is a contract violation. The chosen control proxy request ID
is identical in the action request and forwarded control payload so audit and
response correlation cannot diverge.
Runtime Cloud desired-configuration reconciliation contract¶
Runtime Cloud desired configuration is also a truST product contract outside IEC 61131-3. A desired write has a nonempty actor and an object-valued merge patch. Optional expected revision and ETag values are optimistic-concurrency preconditions; a mismatch rejects the write as a revision conflict. An accepted write advances the desired revision, records a new ETag and writer, and becomes pending until the runtime control operation confirms it.
Reconciliation applies an immutable snapshot containing the desired value,
revision, and ETag. The control operation runs without holding the configuration
state lock. If another desired write is accepted while that operation is in
flight, the older completion may acknowledge only the snapshot it actually
applied: it must not copy the newer desired value into reported, attach the
older revision or ETag to newer content, or mark the agent in sync. The newer
desired revision remains pending and the next reconciliation attempt applies
it. A failed apply preserves the last confirmed reported value and revision and
surfaces a blocked or error state with the stable reason available from the
control failure. If that failure belongs to a superseded snapshot, its
diagnostic remains visible but the newer desired revision stays pending and
eligible for the next reconciliation attempt.
Runtime Cloud contract schema and canonical keyspace¶
Runtime Cloud API versions use the decimal <major>.<minor> form. Equal
versions are exact, versions with the same major are additive-compatible, and
different majors are breaking. Mesh schema evolution is forward-additive only:
field names are nonempty and unique, every field has a nonzero representable
offset + size range, and field ranges do not overlap. Every previous field
must remain present with exactly the same offset, size, and type. New fields may
start only at or after the end of the complete previous layout; inserting one
into an existing field or padding range is not an additive append.
Canonical Runtime Cloud keys are assembled from already validated nonempty
site, runtime, group, and identity segments. The root is
truST/<site>/<runtime>. Runtime-local zones are _meta, io, cmd, cfg,
diag, and svc; _meta, diag, and svc are reserved. The site-level
active slot may be published only by the current Active authority. Desired,
reported, status, and metadata configuration documents use the authoritative
site-level and runtime-alias paths defined by the keyspace helpers. Default UI
and operations staleness is max(2 * expected_period, 2 seconds). A retained
last value may support UI continuity but never a control path.
Runtime Cloud topology measurement and same-host contract¶
Runtime Cloud topology is a truST product projection, not an IEC decision or
deviation. Discovery heartbeat and reachability observations may establish a
node's lifecycle and a communication edge's healthy, stale, degraded, or failed
state. They are not latency or packet-loss measurements. Unless the projection
receives an actual measurement for a metric, latency_ms_p95 and loss_pct
remain absent; it must never manufacture a nominal constant for a healthy edge.
The T0_HardRt topology overlay is same-host only. Equal nonempty discovery
host_group values are positive same-host evidence; different explicit groups
are negative evidence. Without complete host-group evidence, an exact shared
advertised non-loopback IP address is positive evidence. A loopback address is
host-local and therefore proves only that the advertising runtime is colocated
with the dashboard's known local runtime. It must not classify two arbitrary
peer runtimes as colocated, and the result must not depend on source/target or
runtime-ID ordering.
A realtime preference creates a T0_HardRt overlay only while that same-host
evidence holds. The ordinary mesh operations edge may remain visible alongside
the overlay, but it is not a deterministic fallback for T0 and must not be
reported as one.
Runtime Cloud HA lease, split-brain, and replay contract¶
Runtime Cloud dual-host HA is a truST product safety contract outside IEC
61131-3. An action that addresses the active namespace, and every
cmd_invoke, requires a nonempty HA group, an external consistent lease
authority, an available lease, one ACTIVE target whose runtime ID is the lease
owner, a valid nonempty fencing token, and no ambiguous leadership. Loss of any
authority or fence condition denies the protected action before side effects
and requires the affected runtime to remain or become demoted_safe.
The supplied payload.ha.targets map is the asserted complete authority view
for the HA group, not merely a description of the action's target subset. If
more than one entry simultaneously satisfies every ACTIVE lease-owner and
fencing condition, the group is split-brain. Every protected action is denied
with lease_unavailable, including an action whose target_runtimes names
only one of those candidates. Selecting one candidate in the action must never
hide another valid ACTIVE owner from the split-brain check.
Dual-host commands use a positive command_seq per group and runtime. A
completed request ID with the same sequence returns its stored result without
performing the control operation again; reusing that request ID with a
different sequence or submitting a stale sequence is a conflict. While the
first dispatch for a request ID is still owned by the current live coordinator,
another begin attempt is also a conflict and cannot start a second control
operation. A serialized pending request carries no live-process ownership; on
recovery, the same request ID and sequence may be resumed once for
reconciliation and then becomes subject to the ordinary completed-result
deduplication rule.
HMI customization (implementer-specific):
- hmi.schema.get returns theme, pages, and widget-level layout metadata (page, group, order, unit, bounds) in addition to stable widget IDs.
- Project-level hmi.toml supports:
- [theme] (style, optional accent)
- [write] (enabled, allow) for explicit writable-target allowlists.
- [[pages]] (id, title, order)
- [widgets.\"<path>\"] overrides for label/unit/bounds/widget/page/group/order.
- ST-level @hmi(...) annotations on variable declarations support label, unit, min, max, widget, page, group, and order.
- Merge precedence is deterministic: defaults < ST annotations < hmi.toml overrides.
- Theme fallback is deterministic: unknown/missing theme values fall back to built-in classic.
- hmi.write remains disabled unless [write].enabled = true, and writes are accepted only for explicit allowlist matches (id or path) with control authz enforcement.
Operational UX and pairing flow are documented internally.
6.13 Debugging and Diagnostics¶
The runtime emits structured events for debugging and testing: - Cycle start/end (with timestamp) - Task start/end (with task name, priority) - Breakpoint hit / step events (statement boundaries) - Fault and overrun notifications
These events are consumed by the debugger (trust-debug) and test harnesses to validate behavior deterministically.
7. Build Configuration¶
7.1 Feature Flags¶
[features]
default = ["debug"]
debug = [] # enable debug instrumentation and runtime events
7.2 Conditional Compilation¶
Desktop builds use the standard library unconditionally. Embedded support will introduce additional cfg gates for alternative clock implementations.
8. Why Not Alternatives¶
| Alternative | Reason for rejection |
|---|---|
| Containers | Cannot run on microcontrollers. Adds complexity without benefit for this use case. |
| FreeRTOS POSIX simulator | Adds unnecessary layer on desktop. Not production-grade. |
| Embassy (async Rust) | Cooperative scheduling unsuitable for deterministic PLC timing. |
| WASM | Adds complexity. Real-time I/O interaction awkward. Could be future target. |
| Transpile to C | Loses runtime flexibility. Debugging harder. |
9. Future Considerations¶
WebAssembly target: The runtime core (executor, timers) could compile to WASM for browser-based simulation. Would require a WASM-friendly Clock implementation.
Remote I/O: Process image exchange via Modbus TCP is architecturally separate from the clock layer. Networking abstraction would be added alongside the Clock trait, not replacing it.
Retain variables: Persistent storage across power cycles requires platform-specific implementation (filesystem on desktop, flash on embedded). This is orthogonal to the Clock trait and would be added as a separate storage interface if needed.
10. Summary¶
The thin clock abstraction approach provides:
- IEC-aligned task scheduling (periodic and event tasks with defined priority rules)
- Minimal clock surface - easy to maintain and verify
- Clear separation - runtime logic vs clock primitives
- Deterministic behavior - explicit scheduling and I/O latching rules
- Testability - full runtime runs natively on development machine
The runtime is implemented in Rust, using the standard library for desktop targets initially. Embedded backends are planned with identical runtime logic and alternate clock implementations.