The on-demand routes returned ControlError.reason verbatim. Every reason the
client raises is a fixed code, but the display's error code arrives over the
socket, so map the value onto the known set (transport reasons plus
ErrorCode) and report anything else as "other". Resolves CodeQL
py/stack-trace-exposure on the on-demand stop/start responses.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The display process now serves a Unix socket, /run/ledmatrix/control.sock,
carrying versioned newline-delimited JSON commands that are acknowledged.
Stage 1 moves on-demand start/stop (plus status, hello and ping) onto it;
the cache-file mailbox stays as the fallback for one release.
- src/ipc/contract.py: typed request/response envelopes, command args,
error codes, NDJSON framing with a 64 KiB limit, socket path rules.
- src/ipc/server.py: threaded server owned by the display. Handlers only
queue onto a bounded queue and ack with the request id; the render
thread drains it where it reads the mailbox. Bounded clients, timeouts,
garbage/oversize/disconnect handling; 0660 socket in the cache dir's
group plus SO_PEERCRED checks; skips cleanly on Windows or when off.
- src/ipc/client.py: one short-timeout request; any failure raises
ControlError(reason).
- api_v3/display.py: on-demand start/stop try the socket, fall back to
the mailbox exactly as before, and report transport/socket_error.
- display_controller.py: start/close the server; the mailbox handler body
is extracted into _handle_on_demand_request and shared by both paths.
- docs/IPC_CONTROL_SOCKET.md: protocol, security model, stage plan.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>