fix(alarms,acl): one condition event per Part 9 method call; gate Server-object model-change/audit events (#587, #588)

#587 — a client Acknowledge/Confirm/AddComment/Shelve/Enable/Disable emitted no condition event: the SDK's
ReportStateChange is gated on AutoReportStateChanges (never set), and the engine re-projection found the node
already in the new state and was suppressed by the delta-gate. WireMethodTransitionReporting now wraps each
method's SDK handler and, on Good, reports exactly once via ReportConditionEvent, under Lock so the routed
re-projection cannot race in ahead of the SDK applying the state. Independent of the SDK's imprecise
AreEventsMonitored bookkeeping. The shelve methods' state is applied by the wrapper (the SDK never applies
it). Native conditions' Confirmed/Shelving are method-owned (NativeAlarmProjector's constants would re-report
every ack and undo every shelve). MaxTimeShelved set to int.MaxValue ms at materialise — Create() left it 0
and every TimedShelve failed BadShelvingTimeOutOfRange. Wrong "SDK auto-fires (E2)" comments fixed.

#588 — with ACL on, GeneralModelChange and AuditWriteUpdate events no longer go to Server.ReportEvent
(ungated to every i=2253 item): ReportToServerObject delivers them per item, a model change narrowed to the
Affected nodes the session may Browse (dropped when none), an audit write only with Read on its source plus
the SDK audit/encryption guard. Flag off: unchanged.

#586 review: materialise raises only the new condition's monitored unit (was a full O(N) reconcile per
condition ⇒ O(N²) deploys under Lock).

Claude-Session: https://claude.ai/code/session_01Ef2ozxS7QPanGT8uoATbTe
This commit is contained in:
Joseph Doherty
2026-10-08 00:54:08 -04:00
parent 390c8269a0
commit 167a48bffa
10 changed files with 1454 additions and 66 deletions
+27 -8
View File
@@ -564,18 +564,37 @@ Behaviours worth knowing before enabling it:
condition reports through itself (`AuditConditionAcknowledgeEventType`, Comment, Enable, Shelving)
reach a Server-object item only while the server is auditing, over SignAndEncrypt or https-binary,
which is exactly the SDK's own guard. And while any Server-object item exists, every condition counts as
**monitored** (`ConditionState.EventsMonitored()`), so the SDK still reports its method-driven state
changes on a server whose only subscribers are Server-object items. Both were caught in review: the
**monitored** (`ConditionState.EventsMonitored()`), which gates the SDK's method **audit** events on a
server whose only subscribers are Server-object items. Both were caught in review: the
first cut queued `i=8944` to every Server-object item with auditing off, and left conditions
unmonitored. The second also needed the SDK's `AddRootNotifier` neutralised: its "subscribe existing
all-events items with `unsubscribe: true`" bug would otherwise take the unit back from every
condition rebuilt under a live item.
condition rebuilt under a live item. (Method-driven **state-change** events no longer depend on this
bookkeeping at all — the node manager reports them itself, Gitea #587; see `docs/AlarmTracking.md`.
The SDK's counter stays imprecise in one known way — a folder subscriber's unit never reaches a
condition materialised under that folder later, yet its unsubscribe still lowers it — which can now
only cost an audit event, and only while auditing is on.)
- Materialising a condition raises only **that** condition's monitored unit; the full reconcile (every
condition) runs on removal, rebuild and Server-object subscribe/delete. A deploy of N alarms under a
live Server-object item is therefore N bookkeeping steps, not N²/2 under the node-manager lock.
Residual, not closed by #586: **audit and model-change events** (`AuditWriteUpdateEvent`,
`GeneralModelChangeEvent`, …) are still reported straight to the Server object and reach every
Server-object item ungated. They are delivered only when auditing / model-change reporting is on (and
audit events only over an encrypted channel), and a node manager has no seam on the core Server
object's fan-out to filter them.
**Model-change and audit events on the Server object are gated too** (Gitea #588). The node manager's
own server-wide events — `GeneralModelChangeEvent` (NodeAdded / NodeDeleted / DataTypeChanged) and the
`AuditWriteUpdateEvent` of a rejected device write — used to go straight to `Server.ReportEvent`, where
the core Server object queued them to every Server-object item ungated: a session with no grant learned
the RawPath / UNS path of every node added, deleted or retyped, and (with auditing on) the written and
prior values of a rejected write. With the flag **on**, the node manager now delivers them itself, item by
item, to the Server-object items it already holds for #586 (the same audience — only Server-object items
ever received them):
- A **model-change** event keeps only the changes whose `Affected` node the session may **Browse**
(ancestors of a granted node included, as for Browse itself). Every event the server raises today carries
one change, so in practice it is delivered whole or not at all; a multi-change event would be delivered
as a copy holding only the visible changes, with a neutral `Message` (the original names the nodes).
- An **audit write** event needs `Read` on its source node — it carries the node's prior value and the
attempted one — on top of the SDK's own guard (server auditing on, SignAndEncrypt or https-binary).
Auditing stays **off by default**: this repo never sets `ServerConfiguration.AuditingEnabled`.
- Any other server-wide event type is withheld (fail closed); the node manager raises none today.
- With the flag **off**, these events go to `Server.ReportEvent` exactly as before.
- **An ancestor of a granted node is browsable**, or no narrow grant would be reachable: a client
arrives at a deep node by browsing its ancestors, and the trie otherwise inherits downward only. Note
what this necessarily discloses — the *names* of the folders on the path to a granted node are visible