Three review findings, all "unbounded work/memory from untrusted broker input"
rather than correctness bugs - but this points at a live plant broker.
1. InferPayload decoded the WHOLE payload on MQTTnet's dispatcher thread before
trimming to 64 chars. The node cap never engages here: a multi-MB blob on an
already-known topic creates no node, it just updates one, so the cost repeats
per message forever. Now at most PayloadInspectMaxBytes (512) are examined.
A blind slice can cut a multi-byte sequence, which the strict decoder would
report as "binary" - TrimToUtf8Boundary backs the window off to the last whole
sequence so good UTF-8 is never mislabelled. A clipped payload is String by
construction rather than inferred from a partial view.
2. The node cap bounded COUNT, not bytes. MQTT topics run to ~65KB, so 50k nodes
near that ceiling is a multi-GB tree. Topics now bounded at 1024 chars and
segments at 256, rejected whole (a truncated topic is a DIFFERENT topic the
picker would commit as a tag address) and surfaced via an __oversized__ marker
kept distinct from __truncated__ - the operator's remedy differs.
3. Control characters in a snippet would break the picker's single-line row;
Trim() only strips the ends. Now scrubbed to spaces.
Each guard was falsified independently by neutering it and confirming exactly one
test reddens. That found a real gap: the oversized-topic test was passing via the
SEGMENT bound, leaving the whole-topic bound untested - the test now uses many
short segments so only the topic bound can reject it.
Also records the .Driver project reference as a known, deliberate exception to the
.Browser -> .Contracts pattern, with its cost (AdminUI gains Core + Polly + Serilog)
and the clean fix (a leaf transport project; NOT a move into .Contracts, which is
deliberately transport-free).
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
MQTT has no discovery protocol, so the AdminUI address picker learns the topic
space the only way there is: subscribe to the wildcard and accumulate what
arrives. MqttBrowseSession serves that observation as a segment tree (split on
'/'); MqttDriverBrowser opens it with a distinct "{clientId}-browse-{guid8}"
identity under a 5-30 s clamped budget.
Browsing publishes NOTHING - the load-bearing safety property, since the picker
runs against a live plant broker. Every outbound message must route through the
single PublishAsync seam that counts into PublishCountForTest; P2's operator-
triggered Sparkplug rebirth is the one intended exception. Sparkplug mode fails
fast rather than serving a raw spBv1.0 topic tree that looks like a metric tree.
Deviation from the plan's ref list: the project also references .Driver so the
browse CONNECT reuses MqttConnection.BuildClientOptions (TLS posture, CA-pin
chain validator, credentials). A second copy would be a security divergence.
Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW