# Phase 3 — OPC UA standards completeness (H4 + H2-bit + H6) Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task.

**Goal:** Let an OPC UA client Enable/Disable a Part 9 condition (H4); give `HistoryUpdate` its own
permission bit so a HistoryRead grant can't authorize it (H2-bit); route a *native* (Galaxy) condition's
Acknowledge to the owning driver → AVEVA, carrying the authenticated principal (H6).

**Architecture:** All in the OPC UA node-manager + alarm command path + driver inbound-ack route. The engine
already handles Enable/Disable; only the `OnEnableDisable` node-manager delegate is missing. Native vs scripted
conditions are distinguished by a new `bool isNative` threaded through the single shared
`MaterialiseAlarmCondition` (Phase7Applier already knows which is which); native acks route through a new
`NativeAlarmAckRouter` → `DriverHostActor` (inverse condition→driver map) → `driver.AcknowledgeAsync`.

**Tech Stack:** C# / .NET 10, OPC UA Foundation UA-.NETStandard (CustomNodeManager2 / AlarmConditionState),
Akka.NET, xUnit + Shouldly. Design: `docs/plans/2026-06-15-stillpending-phase-3-opcua-standards-design.md`.

**Branch:** `feat/stillpending-phase-3-opcua-standards` (created off master `4af8e65a`; design committed `40b883ef`).

---

## Hard rules (every task)

- Stage **by path** — never `git add .`. Never stage `sql_login.txt`, `src/Server/.../Host/pki/`,
  `pending.md`, `current.md`, `docker-dev/docker-compose.yml`, `stillpending.md`. Never echo/commit secrets
  (gateway API key). No force-push, no `--no-verify`.
- **NO** Configuration entity / EF migration. The ONLY contract touches are additive: `bool isNative` on the
  internal `IOpcUaAddressSpaceSink.MaterialiseAlarmCondition` seam, and `string? OperatorUser` on the
  `AlarmAcknowledgeRequest` Core.Abstractions record. **`NodePermissions` is an int-backed `[Flags]` enum
  (`NodeAcl.PermissionFlags` is an int column) — adding a member is NOT a schema change and needs NO migration.**
- TDD fail-then-pass. xUnit + Shouldly. **NO bUnit.** Cross-node/runtime behavior proven only by live `/run`.
- Production projects are `TreatWarningsAsErrors` — fix all warnings (incl. XML docs on new public members).

## Verified facts the implementer needs

- `NodePermissions` (`src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodePermissions.cs`): `Browse=1<<0` …
  `MethodCall=1<<11`; composites `ReadOnly/Operator/Engineer/Admin`. Highest used bit is `1<<11`.
- `OpcUaOperation` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/OpcUaOperation.cs`) ALREADY has a
  `HistoryUpdate` member.
- `TriePermissionEvaluator.MapOperationToPermission` (`src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs:78-86`)
  maps `HistoryUpdate => NodePermissions.HistoryRead` with a `// TODO` — the bug.
- `MaterialiseAlarmCondition` is the SINGLE shared materializer. Interface
  `IOpcUaAddressSpaceSink.MaterialiseAlarmCondition(alarmNodeId, equipmentNodeId, displayName, alarmType, severity)`;
  impls: `DeferredAddressSpaceSink.cs:47`, `SdkAddressSpaceSink.cs:45`, `NullOpcUaAddressSpaceSink` (in
  Commons/OpcUa), `OtOpcUaNodeManager.cs:538`. Phase7Applier calls it at `:204` (native equipment-tag alarms,
  `tag.Alarm`) and `:295` (scripted, `alarm.ScriptedAlarmId`).
- `OtOpcUaNodeManager.MaterialiseAlarmCondition` wires the inbound method delegates at `:594-624`
  (`OnAcknowledge`/`OnConfirm`/`OnAddComment`/`OnShelve`/`OnTimedUnshelve` → `HandleAlarmCommand`). `OnEnableDisable`
  is absent. `HandleAlarmCommand` (`:654-679`) AlarmAck-gates via `RoleCarryingUserIdentity` and routes via the
  `Action<AlarmCommand>? AlarmCommandRouter` property (`:91`). `AlarmCommand` vocab already has `Enable`/`Disable`.
- The engine already handles them: `ScriptedAlarmEngine.EnableAsync/DisableAsync`,
  dispatched in `ScriptedAlarmHostActor.cs:408-412` on `AlarmCommand` `"Enable"`/`"Disable"`.
- `DriverHostActor` forward map `_alarmNodeIdByDriverRef: (DriverInstanceId, FullName) → condition NodeId(s)`
  (field `:127`, cleared+rebuilt `:902-904`); `ForwardNativeAlarm` (`:537`) resolves on `msg.Args.ConditionId`
  (= dotted FullName). No inbound OPC-UA-ack→driver path exists.
- `AlarmAcknowledgeRequest` (`src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs:44`) =
  `(string SourceNodeId, string ConditionId, string? Comment)` — NO principal field.
- `GalaxyDriver.AcknowledgeAsync` (`src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs:1095`) forwards
  to `IGalaxyAlarmAcknowledger.AcknowledgeAsync(alarmFullReference, comment, operatorUser, ct)` (which already
  takes `operatorUser`). `ScriptedAlarmSource.cs:75` hardcodes `"opcua-client"`.
- Test homes: `tests/Core/.../Authorization/TriePermissionEvaluatorTests.cs` (H2);
  `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs` (H4/H6 node-manager seam);
  `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/...` (DriverHostActor). Galaxy: `tests/Drivers/...Driver.Galaxy.Tests`.

---

### Task 1: H2-bit — `NodePermissions.HistoryUpdate` + evaluator mapping

**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 2

**Files:**
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodePermissions.cs`
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs:86`
- Test: `tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Authorization/TriePermissionEvaluatorTests.cs`

**Step 1: Failing tests** (add to `TriePermissionEvaluatorTests.cs`):
- `HistoryRead_grant_does_not_authorize_HistoryUpdate`: build a scope/ACL granting only
  `NodePermissions.HistoryRead`; `Authorize(session, OpcUaOperation.HistoryUpdate, scope)` → NOT Allow
  (NotGranted). (Mirror an existing test's ACL/session construction.)
- `HistoryUpdate_grant_authorizes_HistoryUpdate`: a scope granting the new `NodePermissions.HistoryUpdate` bit →
  `Authorize(…, HistoryUpdate, …)` → Allow.
- `HistoryUpdate_bit_does_not_collide`: `((int)NodePermissions.HistoryUpdate).ShouldBe(1 << 12)` and
  `(NodePermissions.HistoryUpdate & NodePermissions.MethodCall).ShouldBe(NodePermissions.None)`.

**Step 2: Run, expect FAIL** (the bit doesn't exist / HistoryUpdate currently maps to the HistoryRead bit so the
first test wrongly passes-as-Allow):
`dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests --filter "FullyQualifiedName~TriePermissionEvaluator"`

**Step 3: Implement.**
- In `NodePermissions.cs` add `HistoryUpdate = 1 << 12,` after `MethodCall = 1 << 11,` (with an XML/`//` doc).
  Do NOT add it to any composite (`ReadOnly/Operator/Engineer/Admin`) — the HistoryUpdate *service* is deferred,
  so no role needs to grant it yet; the bit exists only to close the HistoryRead⇒HistoryUpdate hole.
- In `TriePermissionEvaluator.cs:86` change `OpcUaOperation.HistoryUpdate => NodePermissions.HistoryRead,` to
  `OpcUaOperation.HistoryUpdate => NodePermissions.HistoryUpdate,` and delete the stale `// TODO` comment.

**Step 4: Run, expect PASS.** Also `dotnet build ZB.MOM.WW.OtOpcUa.slnx` clean.

**Step 5: Commit**
```bash
git add src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NodePermissions.cs \
        src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/TriePermissionEvaluator.cs \
        tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Authorization/TriePermissionEvaluatorTests.cs
git commit -m "fix(authz): give HistoryUpdate its own NodePermissions bit (was aliased to HistoryRead) [H2]"
```

---

### Task 2: H6a — mark native conditions (`isNative` through the sink)

**Classification:** high-risk
**Estimated implement time:** ~5 min
**Parallelizable with:** Task 1, Task 4

**Files:**
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IOpcUaAddressSpaceSink.cs` (or wherever the interface
  lives — `grep -rn "void MaterialiseAlarmCondition" src --include=*.cs` to confirm; the design noted the sink
  interface declares it)
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/SdkAddressSpaceSink.cs:45`
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/DeferredAddressSpaceSink.cs:47`
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Commons/OpcUa/NullOpcUaAddressSpaceSink.cs` (no-op impl)
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs:538` (signature + `_nativeAlarmNodeIds` set)
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs:204,295,336-339` (`SafeMaterialiseAlarmCondition`)
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs` (+ `SdkAddressSpaceSinkTests` if it asserts the signature)

**Step 1: Failing test.** In `AlarmCommandRouterTests.cs`, add a test that a condition materialised with
`isNative: true` is tracked as native (assert via behavior introduced in this task: e.g. a public/internal
`IsNativeAlarm(nodeId)` test hook on the node manager, OR defer the behavioral assertion to Task 5 and here
just assert the signature compiles + Phase7Applier passes the flag). Prefer adding an `internal bool
IsNativeAlarmNode(string alarmNodeId)` test accessor on `OtOpcUaNodeManager` and asserting
`true` after a native materialise / `false` after a scripted one.

**Step 2: Run, expect FAIL** (param/accessor missing).

**Step 3: Implement.**
- Add `bool isNative = false` as the LAST, defaulted param to `MaterialiseAlarmCondition` on the interface +
  every impl. Defaulted so no other caller breaks.
- `OtOpcUaNodeManager`: add `private readonly HashSet<string> _nativeAlarmNodeIds = new();`; when
  `isNative`, `_nativeAlarmNodeIds.Add(alarmNodeId)`. Add `internal bool IsNativeAlarmNode(string alarmNodeId) =>
  _nativeAlarmNodeIds.Contains(alarmNodeId);`. (Clear the set on address-space rebuild if the manager resets
  `_alarmConditions` — match that lifecycle.)
- `Phase7Applier.SafeMaterialiseAlarmCondition`: add a `bool isNative` param; pass `isNative: true` at the
  native equipment-tag-alarm call (`:204`) and `isNative: false` at the scripted call (`:295`).

**Step 4: Run, expect PASS** + `dotnet build ZB.MOM.WW.OtOpcUa.slnx` clean (all sink impls updated).

**Step 5: Commit** (stage each modified file by path).
`git commit -m "feat(alarms): thread isNative through MaterialiseAlarmCondition; node manager tracks native conditions [H6a]"`

---

### Task 3: H4 — wire `OnEnableDisable`

**Classification:** high-risk
**Estimated implement time:** ~5 min
**Parallelizable with:** none

**Files:**
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs` (in `MaterialiseAlarmCondition`, near `:594-624`)
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs`

**Step 0 — VERIFY the SDK delegate.** Confirm `AlarmConditionState`/`ConditionState` exposes
`OnEnableDisable` and its exact delegate signature (likely `ServiceResult (ISystemContext context,
ConditionState condition, bool enabling)`, matching the `OnShelve`/`OnAcknowledge` settable-handler pattern).
Use IntelliSense/decompile (`Opc.Ua.Server` 1.5.x). If the SDK instead exposes separate `OnEnable`/`OnDisable`
handlers, adapt — wire both to the same body with the right `enabling` value. If `OnEnableDisable` genuinely
does not exist on this SDK version, STOP and report (the design assumed it per `AlarmCommand.cs:37`).

**Step 1: Failing tests** (mirror the existing `OnAcknowledge`/`OnShelve` tests in `AlarmCommandRouterTests.cs`):
- `OnEnableDisable_with_AlarmAck_routes_Disable_command`: materialise a SCRIPTED condition (`isNative:false`);
  capture `AlarmCommandRouter`; invoke `OnEnableDisable(ctx-with-AlarmAck-RoleCarryingUserIdentity, condition,
  enabling:false)` → result `Good`, captured `AlarmCommand.Operation == "Disable"`, `User == DisplayName`.
- `OnEnableDisable_enabling_routes_Enable_command`: same with `enabling:true` → `"Enable"`.
- `OnEnableDisable_anonymous_is_denied`: no/role-less identity → `BadUserAccessDenied`, no route.
- `OnEnableDisable_on_native_condition_is_BadNotSupported`: materialise with `isNative:true`; invoke
  `OnEnableDisable` → `BadNotSupported`, no route. (Uses Task 2's `_nativeAlarmNodeIds`.)

**Step 2: Run, expect FAIL.**

**Step 3: Implement.** In `MaterialiseAlarmCondition`, after the `OnShelve`/`OnTimedUnshelve` wiring:
```csharp
alarm.OnEnableDisable = (context, condition, enabling) =>
{
    // Native (driver-fed) conditions have no engine enable/disable surface (decision #2).
    if (_nativeAlarmNodeIds.Contains(alarmNodeId))
        return new ServiceResult(StatusCodes.BadNotSupported);
    return HandleAlarmCommand(context, condition, enabling ? "Enable" : "Disable", comment: null, unshelveAt: null);
};
```
(Adapt the delegate shape to Step 0's finding. `_nativeAlarmNodeIds` is populated in Task 2 — this task is
ordered after Task 2.)

**Step 4: Run, expect PASS** + full build clean. Note: `ScriptedAlarmHostActor` Enable/Disable dispatch is
already covered — no engine change needed.

**Step 5: Commit**
`git commit -m "feat(alarms): wire OnEnableDisable over OPC UA (AlarmAck-gated; native→BadNotSupported) [H4]"`

---

### Task 4: H6b — `AlarmAcknowledgeRequest.OperatorUser` + Galaxy forwarding + ScriptedAlarmSource tidy

**Classification:** standard
**Estimated implement time:** ~4 min
**Parallelizable with:** Task 1, Task 2

**Files:**
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/IAlarmSource.cs:44` (record)
- Modify: `src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriver.cs:1095` (forward `OperatorUser`)
- Modify: `src/Core/ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms/ScriptedAlarmSource.cs:65-78`
- Test: `tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/...` (Galaxy ack forwarding) + a Core.Abstractions or
  ScriptedAlarms test for the record/`ScriptedAlarmSource` principal.

**Step 1: Failing tests.**
- `GalaxyDriver_AcknowledgeAsync_forwards_OperatorUser`: with a fake `IGalaxyAlarmAcknowledger`, call
  `AcknowledgeAsync([ new AlarmAcknowledgeRequest("src","cond.alarm","cmt"){ OperatorUser="alice" } ])` (or
  positional) → the fake received `operatorUser == "alice"`. (Find the existing Galaxy acknowledger test/fake.)
- `ScriptedAlarmSource_uses_supplied_principal_else_opcua_client`: a request with `OperatorUser="bob"` →
  engine.AcknowledgeAsync called with `"bob"`; with `OperatorUser=null` → `"opcua-client"`.

**Step 2: Run, expect FAIL** (the field doesn't exist).

**Step 3: Implement.**
- `AlarmAcknowledgeRequest` → add `string? OperatorUser = null` as the LAST positional param (keeps existing
  positional construction working). XML-doc it ("authenticated principal; null on the raw path").
- `GalaxyDriver.AcknowledgeAsync`: pass `a.OperatorUser ?? <existing-fallback>` as `operatorUser` to the
  acknowledger (preserve whatever it currently passes when null).
- `ScriptedAlarmSource.AcknowledgeAsync` (`:75`): `await _engine.AcknowledgeAsync(a.ConditionId,
  a.OperatorUser ?? "opcua-client", a.Comment, ct)`.
- AbCip/FOCAS `AcknowledgeAsync` need no change (they ignore the new field) — confirm they still compile.

**Step 4: Run, expect PASS** + full build clean.

**Step 5: Commit**
`git commit -m "feat(alarms): AlarmAcknowledgeRequest carries OperatorUser; Galaxy/ScriptedAlarmSource honor it [H6b]"`

---

### Task 5: H6c — `NativeAlarmAckRouter` seam + native `OnAcknowledge` routing

**Classification:** high-risk
**Estimated implement time:** ~5 min
**Parallelizable with:** none (needs Task 2's `_nativeAlarmNodeIds`)

**Files:**
- Create: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/NativeAlarmAck.cs` (small record — OpcUaServer-local, smallest scope)
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/OtOpcUaNodeManager.cs` (`NativeAlarmAckRouter` property + native `OnAcknowledge`)
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/AlarmCommandRouterTests.cs`

**Step 1: Failing tests.**
- `Native_OnAcknowledge_routes_to_NativeAlarmAckRouter_not_scripted`: materialise `isNative:true`; capture both
  routers; invoke `OnAcknowledge(ctx-with-AlarmAck, condition, _, comment)` → `Good`; the `NativeAlarmAckRouter`
  got `NativeAlarmAck(conditionNodeId, comment, OperatorUser=DisplayName)`; the scripted `AlarmCommandRouter`
  got NOTHING.
- `Scripted_OnAcknowledge_still_uses_AlarmCommandRouter`: `isNative:false` → `AlarmCommandRouter` gets the
  `AlarmCommand`, `NativeAlarmAckRouter` gets nothing.
- `Native_OnAcknowledge_anonymous_is_denied`: no AlarmAck role → `BadUserAccessDenied`, neither router invoked.

**Step 2: Run, expect FAIL.**

**Step 3: Implement.**
- `NativeAlarmAck.cs`: `public sealed record NativeAlarmAck(string ConditionNodeId, string? Comment, string OperatorUser);`
- `OtOpcUaNodeManager`: add `public Action<NativeAlarmAck>? NativeAlarmAckRouter { get; set; }`.
- In `MaterialiseAlarmCondition`, set `OnAcknowledge` based on native-ness:
```csharp
alarm.OnAcknowledge = isNative
    ? (context, condition, _, comment) => HandleNativeAlarmAck(context, condition, comment)
    : (context, condition, _, comment) => HandleAlarmCommand(context, condition, "Acknowledge", comment, null);
```
  (Capture `isNative` per-condition; don't rely on the set lookup inside the closure unless you prefer it —
  either is fine, but the set lookup also works: `_nativeAlarmNodeIds.Contains(alarmNodeId)`.)
- Add `HandleNativeAlarmAck`: resolve the AlarmAck-gated `RoleCarryingUserIdentity` EXACTLY like
  `HandleAlarmCommand` (fail-closed → `BadUserAccessDenied`); else
  `NativeAlarmAckRouter?.Invoke(new NativeAlarmAck(condition.NodeId.Identifier?.ToString() ?? "",
  comment?.Text, identity.DisplayName ?? ""))`; return `Good`. (Confirm whether the native condition's NodeId
  identifier is the folder-scoped condition node id the inverse map in Task 6 will key by — align with
  `_alarmNodeIdByDriverRef`'s value set.)

**Step 4: Run, expect PASS** + full build clean.

**Step 5: Commit**
`git commit -m "feat(alarms): native condition Acknowledge routes to NativeAlarmAckRouter with principal [H6c]"`

---

### Task 6: H6d — `DriverHostActor` inverse map + native-ack route to the driver

**Classification:** high-risk
**Estimated implement time:** ~5 min
**Parallelizable with:** none (needs Task 4 `OperatorUser` + Task 5 `NativeAlarmAck`)

**Files:**
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverHostActor.cs` (inverse map + `Receive<NativeAlarmAck-or-RouteNativeAck>`)
- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DriverInstanceActor.cs` (a `RouteAlarmAck` receive → `driver.AcknowledgeAsync`) — only if the host routes via the instance actor; otherwise the host calls the driver via its existing handle
- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/...` (mirror an existing DriverHostActor test)

**Step 1: Failing tests.**
- `Inverse_map_resolves_condition_node_to_driver_ref`: after an `ApplyDelta`/composition with a native alarm
  tag, the inverse map yields `(DriverInstanceId, FullName)` for the materialised condition node id.
- `NativeAlarmAck_routes_to_driver_AcknowledgeAsync_with_OperatorUser`: feed the host a native-ack message
  (the Runtime-side envelope the host wires the `NativeAlarmAckRouter` to) referencing a known condition node;
  a stub `IAlarmSource` driver receives `AcknowledgeAsync` with `ConditionId == alarmRef`,
  `OperatorUser == "alice"`, `Comment` passed through. Use the shared `StubDrivers.cs` harness.
- `NativeAlarmAck_unknown_node_is_dropped`: an ack for an unmapped node → logged + dropped, no throw.

**Step 2: Run, expect FAIL.**

**Step 3: Implement.**
- Build the inverse `Dictionary<string, (string DriverInstanceId, string FullName)> _driverRefByAlarmNodeId`
  in the SAME clear-and-rebuild pass that builds `_alarmNodeIdByDriverRef` (`:902-904`): for each
  `(driverId, fullName) → {nodeIds}`, add each `nodeId → (driverId, fullName)`.
- Decide the host's inbound envelope: the node manager's `NativeAlarmAckRouter` is `Action<NativeAlarmAck>`
  wired (in Task 7) to `Tell` a Runtime message into `DriverHostActor`. Define that Runtime message
  (e.g. `DriverHostActor.RouteNativeAlarmAck(string ConditionNodeId, string? Comment, string OperatorUser)`)
  — do NOT reference the OpcUaServer `NativeAlarmAck` type from Runtime if it creates a bad dependency; map at
  the wiring boundary (Task 7). `Receive<RouteNativeAlarmAck>`: look up `_driverRefByAlarmNodeId`; on miss →
  `_log.Debug` + return; on hit → route to the driver via the same mechanism `ForwardNativeAlarm`/the write
  path uses (Tell the `DriverInstanceActor` a `RouteAlarmAck` it turns into
  `driver.AcknowledgeAsync([ new AlarmAcknowledgeRequest(SourceNodeId: <owning object or fullName>, ConditionId:
  fullName, Comment, OperatorUser) ], CancellationToken.None)`, fire-and-forget). Mirror the primary-gating
  the write/ack paths use if applicable.

**Step 4: Run, expect PASS** + full build clean.

**Step 5: Commit**
`git commit -m "feat(alarms): DriverHostActor routes native-condition acks to the owning driver [H6d]"`

---

### Task 7: H6e — wire `NativeAlarmAckRouter` in host DI

**Classification:** high-risk
**Estimated implement time:** ~3 min
**Parallelizable with:** none

**Files:**
- Modify: the host wiring that sets `AlarmCommandRouter` today (`grep -rn "AlarmCommandRouter =" src --include=*.cs`
  — likely `OtOpcUaServerHostedService` / a DI/bootstrap seam). Set `NativeAlarmAckRouter` analogously to a
  fire-and-forget `Tell` of the Runtime `RouteNativeAlarmAck` into the `DriverHostActor` (resolve its `IActorRef`
  the same way the AlarmCommandRouter resolves the ScriptedAlarmHost / mediator).
- Test: extend the existing wiring/smoke test if one asserts `AlarmCommandRouter` is set; else none (covered by
  the live `/run`).

**Step 1: Implement** the router assignment (map `NativeAlarmAck` → the Runtime `RouteNativeAlarmAck` at this
boundary so Runtime needn't reference OpcUaServer's record). Confirm the `DriverHostActor` ref is available at
the wiring site (it's registered in the actor registry).

**Step 2: Build + full test.** `dotnet build ZB.MOM.WW.OtOpcUa.slnx` clean; `dotnet test` on the affected
projects green.

**Step 3: Commit**
`git commit -m "feat(alarms): wire NativeAlarmAckRouter to DriverHostActor in host DI [H6e]"`

---

### Task 8: Docs

**Classification:** small
**Estimated implement time:** ~3 min
**Parallelizable with:** Task 7

**Files:**
- Modify: `docs/ScriptedAlarms.md`, `docs/AlarmTracking.md`
- Modify: `docs/security.md` (HistoryUpdate permission bit note, if the § on NodePermissions warrants it)

**Step 1:** Document: H4 — OPC UA clients can now Enable/Disable Part 9 conditions (AlarmAck-gated; the engine
applies it; native conditions return `BadNotSupported`). H6 — an inbound Acknowledge on a *native* (Galaxy)
condition now routes to the owning driver → AVEVA, carrying the authenticated principal (not the generic
`opcua-client`); `AlarmAcknowledgeRequest` gained `OperatorUser`. H2 — `HistoryUpdate` has its own permission
bit (a HistoryRead grant no longer authorizes it); the HistoryUpdate *service* remains deferred (infra-gated).

**Step 2:** No build needed. Grep for now-false claims (e.g. `AlarmCommand.cs`'s "OnEnableDisable … future
task" comment — update it; `ScriptedAlarmSource.cs`'s "opcua-client" remark).

**Step 3: Commit** by path.
`git commit -m "docs(alarms): OPC UA Enable/Disable wired + native-ack→AVEVA + HistoryUpdate permission bit"`

---

### Task 9: Full build + test + final integration review

**Classification:** high-risk
**Estimated implement time:** ~4 min
**Parallelizable with:** none

**Step 1:** `dotnet build ZB.MOM.WW.OtOpcUa.slnx` clean (TreatWarningsAsErrors).
**Step 2:** `dotnet test` on Core.Tests, OpcUaServer.Tests, Runtime.Tests, Driver.Galaxy.Tests — all green.
**Step 3:** Final integration reviewer: confirm (a) no EF/Configuration migration; the only contract touches are
the `isNative` sink param + `AlarmAcknowledgeRequest.OperatorUser` (both additive/defaulted); (b) native acks
route to the driver and scripted acks/enable-disable still route to the engine (no cross-wiring); (c) the
AlarmAck fail-closed gate holds on every new path (H4 Enable/Disable + H6 native ack); (d) the `NativeAlarmAck`
→ Runtime `RouteNativeAlarmAck` boundary doesn't create a Runtime→OpcUaServer bad dependency; (e) staging
by-path only. Apply actionable findings.
**Step 4:** Update `pending.md` (working-tree only, never staged).

---

### Task 10: Live `/run`

**Classification:** high-risk (verification)
**Estimated implement time:** ~6 min (agent-driven on docker-dev; login disabled — do NOT sign in)

**H4 (fully local-provable):** ensure a scripted alarm exists on the dev config (or author one), deploy, then
`dotnet run --project src/Client/ZB.MOM.WW.OtOpcUa.Client.CLI -- ...` — Disable the condition via the Part 9
Disable method (Client.CLI `shelve`/`ack` style command, or a `call` on the Disable method node) **as a
user holding the AlarmAck role** (LDAP `opc-...` per the data-plane role map) → confirm the condition's
`EnabledState` goes Disabled in the server log / a follow-up read; anon → `BadUserAccessDenied`.

**H6 (local half):** trip a native (Galaxy) alarm, Acknowledge it over OPC UA as an AlarmAck user → confirm
from the central-1 log that the ack ROUTED to the driver (`DriverHostActor` native-ack route log line) with the
authenticated principal. The **AVEVA commit** (gateway → Historian on `10.100.0.48`) is operator-gated — drive
it against the reachable gateway if feasible this session (the Galaxy live-gw smokes pattern), else record it
as operator-gated (like H5).

**Done when:** build clean + `dotnet test` green + H4 live-proven + H6 local route proven (AVEVA commit recorded
as driven-or-operator-gated). Then `finishing-a-development-branch` → merge to master + push.
