# Read/Write Operations > **Rewritten 2026-07-27** against `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs`. > The previous revision described a v2-era `DriverNodeManager` with per-variable `OnReadValue` hooks, > an `AuthorizationGate`/`WriteAuthzPolicy` ACL pair and a `NodeSourceKind` dispatch switch. **None of > those exist** — `OnReadValue`, `WriteAuthzPolicy`, `_sourceByFullRef`, `_writeIdempotentByFullRef` > and `IRoleBearer` all have zero occurrences in `src/`. The read path in particular worked nothing > like the way it was documented. See `deferment.md` §3.1 and §7. ## The shape of it: push for reads, pull for writes The live server is `OtOpcUaNodeManager`, a **push-model** `CustomNodeManager2`. This asymmetry is the single most important thing on this page: - **Reads never reach a driver.** There is no `Read` override and no `OnReadValue` / `OnSimpleReadValue` handler anywhere in the node manager. Driver values are *pushed in* from the Akka actor layer (`DriverInstanceActor` polls or subscribes, `DriverHostActor` fans the value to the raw NodeId and every referencing UNS NodeId), and a client Read is served by the OPC UA SDK straight from the cached node value. A client read therefore costs nothing on the wire to the device, and cannot fail with a device error — it returns whatever quality was last pushed. - **Writes are synchronous pull-through.** A client write runs `OnWriteValue` → role gate → driver. Everything the old page said about `CapabilityInvoker` wrapping OPC UA reads was misplaced: the invoker is real, but it lives on the **driver-actor** side wrapping the poll/subscribe calls. The `OpcUaServer` project does not reference it at all. ## Read path 1. The SDK resolves the NodeId in the Raw (`ns=2`) or UNS (`ns=3`) namespace. 2. It returns the cached `DataValue` — value, `StatusCode` and source timestamp as last pushed. 3. **No authorization check runs.** Not a per-node ACL, not a role check, nothing. Any admitted session — including an Anonymous one — can read every node in the address space. The only gating is the `AccessLevels` bitmask set at materialization, which is a per-node *capability* declaration, not a per-user decision. Authored `NodeAcl` deny rules have **no effect on reads** (or on anything else — see [docs/security.md](security.md) § Data-Plane Authorization). ## Write path — `OnEquipmentTagWrite` The handler is attached in `EnsureVariable` **only when the tag was materialized `writable`**, and re-attached or cleared by `UpdateTagAttributes` on an in-place edit. A non-writable node has no handler, so the SDK rejects the write before any of this runs. 1. **Role gate.** `EvaluateEquipmentWriteGate(identity, gatewayWired)` requires the session identity to be a `RoleCarryingUserIdentity` carrying the `WriteOperate` role. No identity or no role ⇒ `BadUserAccessDenied`, fail-closed. Gate passed but no write gateway wired (admin-only node, pre-boot) ⇒ `BadNotWritable`. - This is a **server-wide** check. It takes no node and no realm: a session that may write one tag may write every writable tag on that node. - `WriteTune` and `WriteConfigure` are **never checked**. `OpcUaDataPlaneRoles` declares only `WriteOperate` and `AlarmAck`; the classification tiers exist in the permission vocabulary but no code path reads them. 2. **Optimistic local apply.** The new value is written to the node so subscribers see it immediately. 3. **Realm-qualified dispatch.** `RealmOf(node.NodeId)` selects Raw or UNS, and the value routes to the backing driver ref through the node write gateway. Both NodeIds for a fanned value resolve to the same driver ref, so a write via either one reaches the same device point. 4. **Write-outcome self-correction.** If the device write fails, the optimistic value is **reverted** on the raw NodeId and every referencing UNS NodeId through the shared fan-out, so a failed write cannot leave a phantom Good value behind. (Galaxy is the exception: its write is fire-and-forget, so it can never surface a write failure.) `OnWriteValue` is invoked by the SDK **while holding the node-manager `Lock`**, so the handler must not block. Anything asynchronous is dispatched fire-and-forget with the revert wired as a continuation. ## Alarm method calls Part 9 condition methods (Acknowledge / Confirm / AddComment / OneShotShelve / TimedShelve / Unshelve) are gated by a second role check requiring `AlarmAck` — one role covering all five methods; the `operation` argument is passed to the router but the gate does not consult it. Scripted alarms go through `HandleAlarmCommand`, driver-fed native alarms through `HandleNativeAlarmAck`; both fail closed with `BadUserAccessDenied`. ## HistoryRead Four overrides — `HistoryReadRawModified`, `HistoryReadProcessed`, `HistoryReadAtTime`, `HistoryReadEvents` — route to the **server-wide** `IHistorianDataSource` (the HistorianGateway-backed reader, or `NullHistorianDataSource` when `ServerHistorian:Enabled=false`, which returns `GoodNoData`). This is not a per-driver `IHistoryProvider` dispatch. **No authorization check runs on any of the four.** Unlike `OnWriteValue`, the SDK does *not* hold the node-manager `Lock` while invoking them, so these paths may await. See [docs/Historian.md](Historian.md). ## Failure isolation Exceptions in a driver capability call are logged and converted to a per-node bad status rather than unwinding into the master node manager, so one driver's outage cannot disrupt sibling drivers in the same process. ## What is *not* enforced anywhere Collected here because the previous revision of this page claimed several of these: | Surface | Check | |---|---| | Read, Browse, TranslateBrowsePaths | none | | CreateMonitoredItems / Subscribe / TransferSubscriptions | none | | HistoryRead (all four variants) | none | | Non-Value attribute writes | none (the handler is on `OnWriteValue`) | | Value write | `WriteOperate` role, server-wide | | Alarm methods | `AlarmAck` role, server-wide | | Per-node ACLs (`NodeAcl` / `PermissionTrie`) | **authored and deployed, never evaluated** | ## Key source files - `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs` — the live node manager; `EnsureVariable` / `WriteValue` / `OnEquipmentTagWrite` / `EvaluateEquipmentWriteGate` and the four HistoryRead overrides - `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Security/RoleCarryingUserIdentity.cs`, `Security/OpcUaDataPlaneRoles.cs` — how roles reach the gates - `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/AddressSpaceApplier.cs` — materialization; where the write handler is attached per realm - `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs` — the push side: value fan-out to raw + UNS NodeIds - `src/Core/ZB.MOM.WW.OtOpcUa.Core/Resilience/CapabilityInvoker.cs` — driver-side resilience pipeline (**not** on the OPC UA read path) - `src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/` — the permission trie + evaluator, built and unit-tested but with **zero production consumers**