feat(mqtt): Sparkplug birth-driven browser tree + scoped Request-rebirth action
Unseals the Sparkplug branch of the MQTT address-picker browser that Task 10 deliberately sealed shut, and adds the first (and only) sanctioned publish on a browse session. - MqttBrowseSession serves Group -> EdgeNode -> [Device] -> Metric in Sparkplug mode, decoded from observed NBIRTH/DBIRTH certificates (the only Sparkplug messages that name their metrics). Node-level metrics hang directly under their edge node -- no synthesised device folder, because the authored address tuple genuinely has deviceId = null for them. Metric node ids use a '::' separator: a metric name may contain '/', so slash-joining would make a node-level metric indistinguishable from a device folder. - AttributesAsync is purely birth-derived in Sparkplug mode. The plain path's UTF-8 inference / payload-snippet machinery does not run and no payload bytes are retained: a Sparkplug body is protobuf and would be reported as "(N bytes, binary)" for every metric in the plant. A datatype ToDriverDataType cannot map is reported as the Sparkplug type name, never coerced. - RequestRebirthAsync(scope) is the one publishing member, reached only by an explicit operator action. It routes through the counted PublishAsync chokepoint via a private RebirthTransport adapter, so PublishCountForTest still proves that OpenAsync/RootAsync/ExpandAsync/AttributesAsync/Observe/ DisposeAsync publish nothing -- now in both modes. A device or metric scope resolves up to its owning edge node (NCMD is node-addressed); group scope enumerates observed edge nodes and is refused whole past MaxGroupRebirthNodes = 32. - BrowserSessionService.RequestRebirthAsync gates on the same DriverOperator policy that gates the picker, evaluated server-side where the action happens rather than only at render time, fails closed, and Info-logs the scope and the published count. Exposed through the new IRebirthCapableBrowseSession contract so "does this session write?" stays a type question. - ToBrowseOptions' Last-Will landmine re-verified: MqttDriverOptions still carries nothing will-shaped (an NDEATH is a broker-published will and would be invisible to PublishCountForTest), now pinned by a reflection guard. Falsifiability: injecting a publish into RootAsync reddens the Sparkplug and plain passivity tests; double-publishing per target reddens all four one-NCMD-per-node assertions. 572/572 MQTT tests, 24/24 AdminUI browsing tests. Claude-Session: https://claude.ai/code/session_01GASWkNEi68FSCtvr6rLoEW
This commit is contained in:
@@ -50,6 +50,27 @@ public interface IBrowserSessionService
|
||||
/// <returns>The attributes of the node.</returns>
|
||||
Task<IReadOnlyList<AttributeInfo>> AttributesAsync(Guid token, string nodeId, CancellationToken ct);
|
||||
|
||||
/// <summary>
|
||||
/// Asks the remote peer(s) addressed by <paramref name="scope"/> to re-announce themselves, on
|
||||
/// a session whose driver offers that action (today: MQTT in Sparkplug B mode, where it is a
|
||||
/// Sparkplug rebirth-request NCMD).
|
||||
/// </summary>
|
||||
/// <param name="token">Registry handle for the open browse session.</param>
|
||||
/// <param name="scope">The driver-specific target — for Sparkplug, a browse node id from the
|
||||
/// session's own tree, or a bare <c>{group}/{edgeNode}</c>.</param>
|
||||
/// <param name="ct">Cancellation token for the operation.</param>
|
||||
/// <returns>The number of request messages published.</returns>
|
||||
/// <remarks>
|
||||
/// <b>The only browse operation that writes to the device network</b>, and therefore the only
|
||||
/// one carrying an authorization check: the caller must satisfy the same
|
||||
/// <c>DriverOperator</c> policy that gates the picker's Browse affordance. Everything else on
|
||||
/// this facade is read-only.
|
||||
/// </remarks>
|
||||
/// <exception cref="BrowseSessionNotFoundException">The token is unknown (or was reaped).</exception>
|
||||
/// <exception cref="UnauthorizedAccessException">The caller is not a DriverOperator.</exception>
|
||||
/// <exception cref="NotSupportedException">This session's driver has no re-announce action.</exception>
|
||||
Task<int> RequestRebirthAsync(Guid token, string scope, CancellationToken ct);
|
||||
|
||||
/// <summary>Removes the session from the registry and disposes it. No-op for unknown tokens.</summary>
|
||||
/// <param name="token">Registry handle for the browse session to close.</param>
|
||||
/// <returns>A task that represents the asynchronous operation.</returns>
|
||||
|
||||
Reference in New Issue
Block a user