Skip to content

Bounded persistent external-process co-simulation

ExternalProcessCosimAdapter keeps one native Unix child for the existing JSONL request/response protocol. A step sends time_ns, dt_ns and scalar inputs; its one response contains scalar outputs. Valid bool, integer, finite float and text signals retain their existing types. Nonfinite request numbers are rejected instead of silently becoming JSON null.

spawn uses a two-second exchange deadline and 65,536-byte request and response limits, including the newline. spawn_with_options accepts explicit timeout and frame limits through ExternalProcessCosimOptions; timeout must be positive and no more than 60 seconds, and frames are 128 through 1,048,576 bytes. The native manifest adapter uses the documented defaults. These are wall-clock IPC safety bounds, separate from model step_ns and simulated time.

Native Unix pipes are nonblocking. One monotonic deadline covers encoding, partial writes, interrupted polling, partial reads and response decoding. Caller strings are borrowed for size/finite preflight before building a JSON tree; a capped writer bounds escaped JSON allocation. Reads use a fixed 4 KiB buffer and a capped response vector (capacity growth is bounded by the selected frame cap's next power of two). JSON nesting is limited by serde_json's default recursion limit. There are no worker threads, channels, background queues or retries. Unsolicited trailing response bytes and non-scalar outputs are protocol errors. The adapter also probes nonblocking stdout immediately after a complete frame and immediately before writing each request. Any byte already queued at those checks is a terminal protocol error, including an extra frame beyond the 4 KiB read boundary or arriving between steps. A valid complete one-shot response followed by EOF remains valid; a later step fails normally. These checks do not claim to identify the provenance of arbitrary bytes a malicious peer sends after the pre-request check. The JSONL protocol has no request correlation field.

Any deadline, I/O, request validation or protocol failure is terminal. The adapter kills its own process group, closes its pipes and polls to reap its own leader for at most 250 ms. Drop uses the same cleanup, also killing descendants that inherited stdio. No foreign process group is touched. A child stuck in an uninterruptible kernel wait can outlive that bounded reap interval; cleanup never turns that host failure into an unbounded wait(). Later steps fail immediately without another request, child restart or replacement battery state.

This implementation deliberately supports native Unix only. Browser/WASM and Windows return an explicit unsupported error at spawn; the browser continues to compile without Unix dependencies. Windows previously exposed the unbounded synchronous process path; it now fails explicitly until a bounded Windows pipe transport is implemented. No bounded Windows behavior is claimed.

Tests: cargo test -p labwired-core --test cosim_external_process_bounds --test cosim_contract --test cosim_canonical_basket. The canonical basket test uses the generated native manifest and the existing Python power wrapper, not a Linux guest or new private battery/sensor implementation.