docs: alarm acknowledgement writes AckMsg as the authenticated operator
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
This commit is contained in:
@@ -126,7 +126,7 @@ powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1
|
|||||||
- **One worker per session** (invariant). Multi-subscriber event fan-out and reconnect-with-replay have shipped and are config-gated: `AllowMultipleEventSubscribers` (default `false`) enables fan-out up to `MaxEventSubscribersPerSession` (default `8`); `DetachGraceSeconds` (default `30`) retains a session after its last subscriber drops so clients can reconnect; `ReplayBufferCapacity` / `ReplayRetentionSeconds` control how much event history the replay ring keeps. Default config is single-subscriber (`AllowMultipleEventSubscribers` off), but detach-grace and replay retention are **on** by default (`DetachGraceSeconds=30`, `ReplayBufferCapacity=1024`, `ReplayRetentionSeconds=300`): a detached session is retained for 30 s and recent events are buffered for reconnect. The reconnect protocol is consumable end-to-end: a resuming `StreamEvents` (via `after_worker_sequence`) that predates the retained ring gets a `ReplayGap` sentinel, and all five official clients surface it as a typed signal. Orphan-worker reattach after a gateway restart is **deferred, not planned** — see `docs/DesignDecisions.md` (Session-Resilience Epic Scope, session-resilience epic Phase 5); the invariant on the next line stands. See `docs/DesignDecisions.md` and `docs/Sessions.md`.
|
- **One worker per session** (invariant). Multi-subscriber event fan-out and reconnect-with-replay have shipped and are config-gated: `AllowMultipleEventSubscribers` (default `false`) enables fan-out up to `MaxEventSubscribersPerSession` (default `8`); `DetachGraceSeconds` (default `30`) retains a session after its last subscriber drops so clients can reconnect; `ReplayBufferCapacity` / `ReplayRetentionSeconds` control how much event history the replay ring keeps. Default config is single-subscriber (`AllowMultipleEventSubscribers` off), but detach-grace and replay retention are **on** by default (`DetachGraceSeconds=30`, `ReplayBufferCapacity=1024`, `ReplayRetentionSeconds=300`): a detached session is retained for 30 s and recent events are buffered for reconnect. The reconnect protocol is consumable end-to-end: a resuming `StreamEvents` (via `after_worker_sequence`) that predates the retained ring gets a `ReplayGap` sentinel, and all five official clients surface it as a typed signal. Orphan-worker reattach after a gateway restart is **deferred, not planned** — see `docs/DesignDecisions.md` (Session-Resilience Epic Scope, session-resilience epic Phase 5); the invariant on the next line stands. See `docs/DesignDecisions.md` and `docs/Sessions.md`.
|
||||||
- **Gateway restart does not reattach orphan workers.** The first version terminates orphaned workers on startup; do not design code paths that assume reattachment.
|
- **Gateway restart does not reattach orphan workers.** The first version terminates orphaned workers on startup; do not design code paths that assume reattachment.
|
||||||
- **No Blazor UI component libraries.** Dashboard uses local Bootstrap CSS/JS only — do not introduce MudBlazor, Radzen, FluentUI, etc.
|
- **No Blazor UI component libraries.** Dashboard uses local Bootstrap CSS/JS only — do not introduce MudBlazor, Radzen, FluentUI, etc.
|
||||||
- **Don't log secrets or full tag values by default.** API keys, passwords, `WriteSecured` payloads, and `AuthenticateUser` credentials must never reach logs. Value logging is opt-in and redacted.
|
- **Don't log secrets or full tag values by default.** API keys, passwords, `WriteSecured` payloads, `AuthenticateUser` credentials, and `AcknowledgeAlarm` `operator_password` must never reach logs. Value logging is opt-in and redacted.
|
||||||
- **Generated code** under `src/ZB.MOM.WW.MxGateway.Contracts/Generated/`, `clients/*/generated*/`, `clients/python/src/zb_mom_ww_mxgateway/generated/`, etc., is build output. Don't hand-edit. To regenerate, build the contracts project (`dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj`) or run the per-client generation step in that client's README.
|
- **Generated code** under `src/ZB.MOM.WW.MxGateway.Contracts/Generated/`, `clients/*/generated*/`, `clients/python/src/zb_mom_ww_mxgateway/generated/`, etc., is build output. Don't hand-edit. To regenerate, build the contracts project (`dotnet build src/ZB.MOM.WW.MxGateway.Contracts/ZB.MOM.WW.MxGateway.Contracts.csproj`) or run the per-client generation step in that client's README.
|
||||||
- **Documentation style** (`StyleGuide.md`): PascalCase filenames, no marketing language, present tense, explain *why* not *what*.
|
- **Documentation style** (`StyleGuide.md`): PascalCase filenames, no marketing language, present tense, explain *why* not *what*.
|
||||||
- **Update docs in the same change as the source.** When public APIs, contracts, configuration, build steps, security behavior, event shapes, value conversion, status mapping, or lifecycle rules change, the affected docs (`gateway.md`, `docs/`, client READMEs, design docs) must change in the same commit. Don't leave stale prose describing old behavior.
|
- **Update docs in the same change as the source.** When public APIs, contracts, configuration, build steps, security behavior, event shapes, value conversion, status mapping, or lifecycle rules change, the affected docs (`gateway.md`, `docs/`, client READMEs, design docs) must change in the same commit. Don't leave stale prose describing old behavior.
|
||||||
|
|||||||
@@ -621,8 +621,9 @@ returned to normal but is unacknowledged — i.e., visible in the
|
|||||||
"current alarms" list because operator hasn't acked it yet) and
|
"current alarms" list because operator hasn't acked it yet) and
|
||||||
`UNACK_ALM` (the alarm is currently active and unacknowledged).
|
`UNACK_ALM` (the alarm is currently active and unacknowledged).
|
||||||
The other states from `eAlmState` (`ACK_RTN`, `ACK_ALM`) would
|
The other states from `eAlmState` (`ACK_RTN`, `ACK_ALM`) would
|
||||||
appear when an ack is performed — `wwAlarmConsumerClass.AlarmAckByGUID`
|
appear when an ack is performed. The alarm-client ack methods
|
||||||
is the method to call.
|
(`AlarmAckByGUID` and friends) do not produce them headless; an `AckMsg`
|
||||||
|
write does (`AlarmProbeFindings.md`).
|
||||||
|
|
||||||
### `GetStatistics` AV — unrelated quirk
|
### `GetStatistics` AV — unrelated quirk
|
||||||
|
|
||||||
@@ -735,25 +736,32 @@ for `SetXmlAlarmQuery`.
|
|||||||
the canonical `\\<machine>\Galaxy!<area>` form into the XML's
|
the canonical `\\<machine>\Galaxy!<area>` form into the XML's
|
||||||
NODE/PROVIDER/GROUP fields. Mangled or not, the call enables reads.
|
NODE/PROVIDER/GROUP fields. Mangled or not, the call enables reads.
|
||||||
|
|
||||||
### 2. Two consumers required: read-side vs. ack-side
|
### 2. Two consumers required: read-side vs. ack-side (historical)
|
||||||
|
|
||||||
|
> Superseded 2026-10-19: acks now write `AckMsg` as the authenticated operator;
|
||||||
|
> see `AlarmProbeFindings.md`. The worker no longer provisions an ack-only
|
||||||
|
> wnwrap consumer. The text below records why the split existed.
|
||||||
|
|
||||||
`SetXmlAlarmQuery` enables reads but **breaks `AlarmAckByName` on
|
`SetXmlAlarmQuery` enables reads but **breaks `AlarmAckByName` on
|
||||||
the same consumer instance**. With SetXml applied, AlarmAckByName
|
the same consumer instance**. With SetXml applied, AlarmAckByName
|
||||||
returns -55 even with valid name+provider+group+operator. Without
|
returns -55 even with valid name+provider+group+operator. Without
|
||||||
SetXml, AlarmAckByName succeeds with rc=0.
|
SetXml, AlarmAckByName succeeds with rc=0.
|
||||||
|
|
||||||
The production consumer therefore provisions **two** wnwrap COM
|
The consumer at the time provisioned **two** wnwrap COM
|
||||||
instances:
|
instances:
|
||||||
- Primary consumer (`client`): runs full lifecycle including
|
- Primary consumer (`client`): runs full lifecycle including
|
||||||
`SetXmlAlarmQuery` for `GetXmlCurrentAlarms2` polls.
|
`SetXmlAlarmQuery` for `GetXmlCurrentAlarms2` polls.
|
||||||
- Ack-only consumer (`ackClient`): runs Initialize → Register →
|
- Ack-only consumer (`ackClient`): runs Initialize → Register →
|
||||||
Subscribe via the v1-prefixed methods, **no SetXmlAlarmQuery**.
|
Subscribe via the v1-prefixed methods, **no SetXmlAlarmQuery**.
|
||||||
All `AcknowledgeByName` calls dispatch through this instance.
|
All by-name ack calls dispatched through this instance.
|
||||||
|
|
||||||
Both consumers subscribe to the same expression. Disposal cleans up
|
Both consumers subscribe to the same expression. Disposal cleans up
|
||||||
both via a shared `ReleaseConsumerCom` helper.
|
both via a shared `ReleaseConsumerCom` helper.
|
||||||
|
|
||||||
### 3. `AlarmAckByName` v2 8-arg vs. v1 6-arg
|
### 3. `AlarmAckByName` v2 8-arg vs. v1 6-arg (historical)
|
||||||
|
|
||||||
|
> Superseded 2026-10-19: the gateway no longer calls `AlarmAckByName`; see
|
||||||
|
> `AlarmProbeFindings.md`.
|
||||||
|
|
||||||
`wwAlarmConsumerClass` exposes two `AlarmAckByName` overloads:
|
`wwAlarmConsumerClass` exposes two `AlarmAckByName` overloads:
|
||||||
- `IwwAlarmConsumer2` v2: 8 args (`name, provider, group, comment,
|
- `IwwAlarmConsumer2` v2: 8 args (`name, provider, group, comment,
|
||||||
@@ -762,8 +770,8 @@ both via a shared `ReleaseConsumerCom` helper.
|
|||||||
|
|
||||||
The v2 8-arg method returns -55 on this AVEVA build regardless of
|
The v2 8-arg method returns -55 on this AVEVA build regardless of
|
||||||
operator-identity inputs — looks like a stub. The v1 6-arg method
|
operator-identity inputs — looks like a stub. The v1 6-arg method
|
||||||
works. Production `WnWrapAlarmConsumer.AcknowledgeByName` calls the
|
works. `WnWrapAlarmConsumer.AcknowledgeByName` called the
|
||||||
6-arg overload and discards the proto's `domain` + `full_name` fields.
|
6-arg overload and discarded the proto's `domain` + `full_name` fields.
|
||||||
|
|
||||||
**Correction, 2026-08-18** (`docs/AlarmProbeFindings.md`). Both halves
|
**Correction, 2026-08-18** (`docs/AlarmProbeFindings.md`). Both halves
|
||||||
of the paragraph above are narrower than they read. The -55 tracks the
|
of the paragraph above are narrower than they read. The -55 tracks the
|
||||||
@@ -775,42 +783,34 @@ snapshot stay `UNACK_ALM`, `OPERATOR_NAME` stay empty, and the alarm
|
|||||||
extension's own `.Acked` attribute stay `False` for 16 s after each
|
extension's own `.Acked` attribute stay `False` for 16 s after each
|
||||||
call. On this rig the wnwrap ack is accepted and then inert, so a zero
|
call. On this rig the wnwrap ack is accepted and then inert, so a zero
|
||||||
return code must not be read as an applied acknowledgement.
|
return code must not be read as an applied acknowledgement.
|
||||||
The proto contract keeps the 8 fields for forward compatibility if
|
|
||||||
AVEVA fixes the v2 method later.
|
|
||||||
|
|
||||||
### 4. `AlarmAckByGUID` is not implemented
|
### 4. `AlarmAckByGUID` is not implemented (historical)
|
||||||
|
|
||||||
|
> Superseded 2026-10-19: acks now write `AckMsg` as the authenticated operator;
|
||||||
|
> see `AlarmProbeFindings.md`.
|
||||||
|
|
||||||
The v2 `AlarmAckByGUID(VBGUID, …)` throws `NotImplementedException`
|
The v2 `AlarmAckByGUID(VBGUID, …)` throws `NotImplementedException`
|
||||||
(COM `E_NOTIMPL`) on `wwAlarmConsumerClass` against this AVEVA
|
(COM `E_NOTIMPL`) on `wwAlarmConsumerClass` against this AVEVA
|
||||||
build. The reference→GUID lookup that we initially planned to wire
|
build. The reference→GUID lookup that we initially planned to wire
|
||||||
through `AlarmAckByGUID` is therefore not viable on wnwrap; all acks
|
through `AlarmAckByGUID` is therefore not viable on wnwrap.
|
||||||
must go through `AlarmAckByName`.
|
|
||||||
|
|
||||||
The proto `AcknowledgeAlarmCommand` (GUID-based) and the worker's
|
#### Managed GUID ack experiment — did not work (live-tested 2026-10-18, removed 2026-10-19)
|
||||||
`MxAccessCommandExecutor.ExecuteAcknowledgeAlarm` switch arm remain
|
|
||||||
in the codebase for the forward-compat shape, but the gateway-side
|
|
||||||
`WorkerAlarmRpcDispatcher.AcknowledgeAsync` now always routes through
|
|
||||||
`AcknowledgeAlarmByName` when the public RPC supplies a recognizable
|
|
||||||
`Provider!Group.Tag` reference.
|
|
||||||
|
|
||||||
**Opt-in managed GUID ack — does not work on this rig (live-tested
|
An opt-in experiment drove `aaAlarmManagedClient.AlarmClient` by reflection
|
||||||
2026-10-18).** Setting `MXGATEWAY_USE_MANAGED_ALARM_ACK=1` in the
|
(its members are non-public) to take GUID-keyed acks in place of the wnwrap
|
||||||
gateway's environment (workers inherit it) makes the worker build an
|
method. The live test showed it could not work, and the experiment and its
|
||||||
`AaManagedAlarmAckConsumer`, which drives
|
enabling flag were removed once the `AckMsg` write replaced it. The findings
|
||||||
`aaAlarmManagedClient.AlarmClient` by reflection (its members are
|
are kept because they explain why the alarm-client route was abandoned.
|
||||||
non-public) and is meant to take GUID-keyed acks in place of the
|
|
||||||
wnwrap method. The live test shows it cannot do that as built, so leave
|
|
||||||
the flag off.
|
|
||||||
|
|
||||||
The test raised `TestMachine_001.TestAlarm001` with a secured write,
|
The test raised `TestMachine_001.TestAlarm001` with a secured write,
|
||||||
read its GUID from the wnwrap XML snapshot (the public API exposes no
|
read its GUID from the wnwrap XML snapshot (the public API exposes no
|
||||||
alarm GUIDs), and acknowledged it through the `AcknowledgeAlarm` RPC
|
alarm GUIDs), and acknowledged it through the `AcknowledgeAlarm` RPC
|
||||||
with the flag on:
|
with the experiment enabled:
|
||||||
|
|
||||||
| Step | Result |
|
| Step | Result |
|
||||||
|---|---|
|
|---|---|
|
||||||
| RPC ack by GUID, flag off (control) | `NotImplementedException`; `.Acked` stays `False` |
|
| RPC ack by GUID, experiment off (control) | `NotImplementedException`; `.Acked` stays `False` |
|
||||||
| RPC ack by GUID, flag on | Same `NotImplementedException`; `.Acked` `False`, `.AckMsg` empty |
|
| RPC ack by GUID, experiment on | Same `NotImplementedException`; `.Acked` `False`, `.AckMsg` empty |
|
||||||
| `AlarmClient.RegisterConsumer` in a process with a registered wnwrap consumer | `-65` — and wnwrap gets `-65` if the managed client registered first; a real `hWnd` does not help |
|
| `AlarmClient.RegisterConsumer` in a process with a registered wnwrap consumer | `-65` — and wnwrap gets `-65` if the managed client registered first; a real `hWnd` does not help |
|
||||||
| Managed client alone in its own process, records loaded | `AlarmAckByGUID` → `-1`, `SelectByGUID` → `-1` for the wnwrap GUID in both byte orders, `AlarmAckSelected` → `-57` |
|
| Managed client alone in its own process, records loaded | `AlarmAckByGUID` → `-1`, `SelectByGUID` → `-1` for the wnwrap GUID in both byte orders, `AlarmAckSelected` → `-57` |
|
||||||
|
|
||||||
@@ -820,7 +820,7 @@ Three separate problems follow:
|
|||||||
a wnwrap reader, so the managed `RegisterConsumer` fails with `-65`.
|
a wnwrap reader, so the managed `RegisterConsumer` fails with `-65`.
|
||||||
The dispatcher is built to drop back to the wnwrap ack path on a
|
The dispatcher is built to drop back to the wnwrap ack path on a
|
||||||
lifecycle failure (so alarm reads keep working), and it does so
|
lifecycle failure (so alarm reads keep working), and it does so
|
||||||
silently — which is why the flag-on RPC is indistinguishable from the
|
silently — which is why the experiment-on RPC was indistinguishable from the
|
||||||
control.
|
control.
|
||||||
2. **The managed client does not recognise the wnwrap GUID.** `-1` from
|
2. **The managed client does not recognise the wnwrap GUID.** `-1` from
|
||||||
`SelectByGUID` is "not found", not a security refusal, even with the
|
`SelectByGUID` is "not found", not a security refusal, even with the
|
||||||
@@ -833,14 +833,8 @@ Three separate problems follow:
|
|||||||
strings, so the domain and full name the managed method carries
|
strings, so the domain and full name the managed method carries
|
||||||
would not arrive through the public RPC even if 1 and 2 were solved.
|
would not arrive through the public RPC even if 1 and 2 were solved.
|
||||||
|
|
||||||
A working managed ack would need the client hosted out of process, a way
|
The route was dropped in favour of the authenticated `AckMsg` write
|
||||||
to resolve the alarm to the identifier the managed client uses, and the
|
(`AlarmProbeFindings.md`, "Fourth attempt").
|
||||||
identity fields plumbed through the gateway. Routing rules the worker
|
|
||||||
already applies stay correct and would carry over: subtag-mode acks
|
|
||||||
(synthetic GUIDs) stay on the subtag consumer, and by-name acks stay on
|
|
||||||
wnwrap because `AlarmClient` has no by-name ack. The skip-gated
|
|
||||||
`AlarmManagedGuidAckLiveSmokeTests` exercises the in-process
|
|
||||||
arrangement and hits problem 1.
|
|
||||||
|
|
||||||
### 5. STA / threading — resolved
|
### 5. STA / threading — resolved
|
||||||
|
|
||||||
@@ -996,20 +990,24 @@ AVEVA-internal GUID.
|
|||||||
|
|
||||||
### Acknowledge in subtag mode
|
### Acknowledge in subtag mode
|
||||||
|
|
||||||
`AlarmDispatcher` routes ack calls by active provider mode:
|
Both provider modes acknowledge through the worker's `AlarmAckWriter`, which
|
||||||
|
calls `AuthenticateUser` as the requesting operator and writes the comment to
|
||||||
|
the ack item over MXAccess on its own `LMXProxyServer`:
|
||||||
|
|
||||||
- **Alarm-manager mode:** `AlarmAckByName` on `wwAlarmConsumerClass` (unchanged).
|
- **Alarm-manager mode:** the item is `<TagName>.AckMsg` for Galaxy-provider
|
||||||
- **Subtag mode:** `SubtagAlarmConsumer.AcknowledgeByName` resolves the
|
alarms. An alarm from another provider is not ackable.
|
||||||
watch-list entry's `ack_comment_subtag` and issues a `Write(comment)` on
|
- **Subtag mode:** `SubtagAlarmConsumer` resolves the watch-list entry's
|
||||||
the STA via `LmxSubtagAlarmSource`. Writing the `AckMsg` subtag performs
|
`ack_comment_subtag` and the writer writes that item verbatim. Writing the
|
||||||
the acknowledge in AVEVA (`AckMsg` is the confirmed `AlarmExtension` ack-comment
|
`AckMsg` subtag performs the acknowledge in AVEVA (`AckMsg` is the confirmed
|
||||||
write target).
|
`AlarmExtension` ack-comment write target).
|
||||||
|
|
||||||
If the alarm has no writable ack-comment subtag (`AckComment` config key is
|
Because the write is authenticated, the Historian records the operator's name,
|
||||||
empty, or the entry's `ack_comment_subtag` field is empty), the ack call
|
domain and full name, and the writer's MXAccess client name
|
||||||
returns a failure code that the gateway surfaces as `FailedPrecondition`.
|
(`MxGateway:Alarms:AckClientName`) as OperatorNode. If the alarm has no ack
|
||||||
`AcknowledgeByGuid` maps the synthetic GUID back to its reference via an
|
target (`AckComment` config key is empty, the entry's `ack_comment_subtag` is
|
||||||
internal dictionary, then calls the same write path.
|
empty, or the reference or synthetic GUID is unknown), the worker reports it
|
||||||
|
as not found or not ackable and the gateway surfaces `InvalidRequest` with
|
||||||
|
`hresult` -1.
|
||||||
|
|
||||||
`SubtagAlarmConsumer.Subscribe` advises the ack-comment subtag alongside the
|
`SubtagAlarmConsumer.Subscribe` advises the ack-comment subtag alongside the
|
||||||
observed ones (active/acked/priority). This is required: MXAccess rejects a
|
observed ones (active/acked/priority). This is required: MXAccess rejects a
|
||||||
@@ -1027,7 +1025,7 @@ The subtag path was validated against live MXAccess on the dev rig
|
|||||||
`….Priority` → `500` (Int32), `….AckMsg` → string — confirming the field
|
`….Priority` → `500` (Int32), `….AckMsg` → string — confirming the field
|
||||||
names **and** the runtime reference shape `<Object>.<AlarmAttr>.<field>`
|
names **and** the runtime reference shape `<Object>.<AlarmAttr>.<field>`
|
||||||
with **no** intermediate alarm-condition segment.
|
with **no** intermediate alarm-condition segment.
|
||||||
- `AcknowledgeByName` (AckMsg write) returned `0` once the ack-comment subtag
|
- The `AckMsg` write returned `0` once the ack-comment subtag
|
||||||
was advised — confirming the ack-by-comment-write mechanism end to end.
|
was advised — confirming the ack-by-comment-write mechanism end to end.
|
||||||
|
|
||||||
### Fidelity limitations
|
### Fidelity limitations
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ the blocker rather than rediscovering it.
|
|||||||
| GUID stable across polls and `ALM → RTN` | Answered — yes (2026-05-01 capture in `AlarmClientDiscovery.md`, reconfirmed 2026-08-18) |
|
| GUID stable across polls and `ALM → RTN` | Answered — yes (2026-05-01 capture in `AlarmClientDiscovery.md`, reconfirmed 2026-08-18) |
|
||||||
| GUID stable across clear-then-re-raise | Answered 2026-08-18 — **no**, a re-raise mints a new GUID |
|
| GUID stable across clear-then-re-raise | Answered 2026-08-18 — **no**, a re-raise mints a new GUID |
|
||||||
| GUID stable across `UNACK → ACK` | **Open** — the rig cannot be driven into an acknowledged state; 2026-08-18 (third attempt) identifies the reason as the test attribute's `MxSecurityOperate` classification, which no non-interactive ack surface on the rig can satisfy |
|
| GUID stable across `UNACK → ACK` | **Open** — the rig cannot be driven into an acknowledged state; 2026-08-18 (third attempt) identifies the reason as the test attribute's `MxSecurityOperate` classification, which no non-interactive ack surface on the rig can satisfy |
|
||||||
|
| Acknowledge an alarm headless | Answered 2026-10-19 — **an `AckMsg` write as the authenticated operator**; every alarm-client ack is inert (see "Fourth attempt") |
|
||||||
| `COUNT` = total active vs records-in-reply under a capped fetch | Answered 2026-08-18 — **records in the reply** |
|
| `COUNT` = total active vs records-in-reply under a capped fetch | Answered 2026-08-18 — **records in the reply** |
|
||||||
|
|
||||||
The 2026-08-17 run below is kept because it is the record of the wrong-verb blocker. The
|
The 2026-08-17 run below is kept because it is the record of the wrong-verb blocker. The
|
||||||
@@ -420,3 +421,73 @@ The second attempt's inventory, unchanged since: the three
|
|||||||
snapshot, since nothing can acknowledge them away. `SnapshotActiveAlarms` counts only
|
snapshot, since nothing can acknowledge them away. `SnapshotActiveAlarms` counts only
|
||||||
`UNACK_ALM` and `ACK_ALM` as active, so these are inert for the gateway; they will clear on
|
`UNACK_ALM` and `ACK_ALM` as active, so these are inert for the gateway; they will clear on
|
||||||
the next `alarmmgr` restart.
|
the next `alarmmgr` restart.
|
||||||
|
|
||||||
|
## Fourth attempt (2026-10-19): authenticated AckMsg write
|
||||||
|
|
||||||
|
The third attempt left the acknowledge leg blocked. This run found a route that
|
||||||
|
acknowledges and identifies the operator, and ruled out the alarm-client routes.
|
||||||
|
It drove `TestMachine_001.TestAlarm001`, raised with
|
||||||
|
`mxa write TestMachine_001.TestAlarm001 true --type bool --secured -u Administrator`.
|
||||||
|
|
||||||
|
### Writing `AckMsg` acknowledges
|
||||||
|
|
||||||
|
Writing the comment to `TestMachine_001.TestAlarm001.AckMsg` over MXAccess
|
||||||
|
acknowledges the alarm: `.Acked` becomes `True` and the wnwrap record moves to
|
||||||
|
`STATE=ACK_ALM` with an `ACK_COMMENT`. `AckMsg` is `MxSecurityFreeAccess` even
|
||||||
|
though the alarm UDA itself is `MxSecurityOperate`, so no Galaxy change is
|
||||||
|
needed.
|
||||||
|
|
||||||
|
What the Historian records, read from `Runtime.dbo.v_AlarmEventHistory2`:
|
||||||
|
|
||||||
|
| Write | Operator | DomainName | UserFullName | OperatorNode | Description |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Unauthenticated `Write` | `NULL` | `NULL` | `NULL` | `NULL` | the comment |
|
||||||
|
| `AuthenticateUser` + `Write(value, userId)` | `Administrator` | `ArchestrA` | `Administrator` | `mxa` (the MXAccess `Register()` client name) | the comment |
|
||||||
|
|
||||||
|
The wnwrap XML also gains `ACK_OPERATOR_NAME`, `ACK_DOMAIN_NAME`,
|
||||||
|
`ACK_OPERATOR_NODE` and `ACK_COMMENT` on the acknowledged record; the worker's
|
||||||
|
snapshot parser reads them for Acknowledge transitions. An unauthenticated write
|
||||||
|
therefore acknowledges but leaves no operator in the Historian, which is why the
|
||||||
|
gateway requires `operator_user` and authenticates it.
|
||||||
|
|
||||||
|
Another actor's `AckMsg` writes on the same day (authenticated as `dohertj2`)
|
||||||
|
logged `Operator=dohertj2`, `Domain="."` and that machine's name as the node,
|
||||||
|
which matches the client-name rule. By this date unauthenticated writes to
|
||||||
|
`MxSecurityOperate` attributes were also accepted Galaxy-wide on the rig, where
|
||||||
|
August refused them with 1008.
|
||||||
|
|
||||||
|
### Alarm-client acks are inert in every variant
|
||||||
|
|
||||||
|
All of these returned `rc=0` or `-1` and changed neither the snapshot, `.Acked`
|
||||||
|
nor the Historian:
|
||||||
|
|
||||||
|
- wnwrap `AlarmAckByName` and `AlarmAckByGUID` (`E_NOTIMPL`) on the ack-only and
|
||||||
|
reader consumers.
|
||||||
|
- wnwrap `IwwAlarmConsumer2` on a sole consumer with `SetXmlAlarmQuery` applied
|
||||||
|
(list populated, `GetAlarm(0)` returns the row): `AlarmAck` with ids
|
||||||
|
0/1/2/3/-1/65536, and `Select(0)` + `AlarmAckSelected`, with operators
|
||||||
|
`TestOperator`, `Administrator` and `dohertj2`. `SelectByGUID` is `E_NOTIMPL`.
|
||||||
|
- The same calls with `TestAlarm001` temporarily reclassified to
|
||||||
|
`MxSecurityFreeAccess` (template and instance, restored afterwards), so the
|
||||||
|
security classification is not the cause.
|
||||||
|
- Managed `aaAlarmManagedClient`: `AlarmAckByGUID` and `SelectByGUID` return
|
||||||
|
`-1` (the client's list never fills headless, even with a real `hWnd` and a
|
||||||
|
message pump); `AckSelectedTagAlarms_Ex` returns `0` even for a
|
||||||
|
`NOSUCHPROVIDER` provider and writes no Historian row. The managed and wnwrap
|
||||||
|
consumers cannot share a process (`RegisterConsumer` returns `-65`).
|
||||||
|
- A 20 s continuous message pump after `AlarmAck`, and registering as `InTouch`,
|
||||||
|
`WindowViewer` or `Wonderware InTouch`.
|
||||||
|
|
||||||
|
Static reverse engineering matched: the five-string COM acks forward domain and
|
||||||
|
full name with an empty password, and InTouch's `AlarmViewer.ocx` also sends an
|
||||||
|
empty password. The user-request call is synchronous request-to-return-code with
|
||||||
|
no completion channel, so the drop happens provider-side. Hooking the user
|
||||||
|
request in a live WindowViewer that acknowledges successfully and diffing it
|
||||||
|
against the headless probe is the one definitive step not taken.
|
||||||
|
|
||||||
|
### Consequence
|
||||||
|
|
||||||
|
The gateway acknowledges by authenticating the operator and writing `AckMsg`
|
||||||
|
(`docs/DesignDecisions.md`, "Alarm acknowledgement writes AckMsg as the
|
||||||
|
authenticated operator"). The managed ack consumer and the second wnwrap ack
|
||||||
|
consumer were removed.
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ public string ResolveRequiredScope(object request)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `_ => GatewayScopes.Admin` fallback is intentional: any future request type that the resolver does not recognize fails closed, requiring the strongest scope until the resolver is updated. `AcknowledgeAlarm` is treated as a write — it mutates alarm state, mirroring `MxCommandKind.Write*` — and `StreamAlarms` and `QueryActiveAlarms` share the alarm/event surface with `StreamEvents` and `MxCommandKind.DrainEvents`, so they carry `events:read` (the active-alarm snapshot is the same data reachable through the event surface). All three alarm RPCs are session-less: the scope check is the only authorization gate, since there is no per-session ownership to enforce.
|
The `_ => GatewayScopes.Admin` fallback is intentional: any future request type that the resolver does not recognize fails closed, requiring the strongest scope until the resolver is updated. `AcknowledgeAlarm` is treated as a write — it mutates alarm state, mirroring `MxCommandKind.Write*` — and stays `invoke:write` rather than `invoke:secure` because the request carries the operator's own Galaxy credential, which the worker authenticates before the `AckMsg` write, so the credential, not the API-key scope, authorizes the identity the Historian records — and `StreamAlarms` and `QueryActiveAlarms` share the alarm/event surface with `StreamEvents` and `MxCommandKind.DrainEvents`, so they carry `events:read` (the active-alarm snapshot is the same data reachable through the event surface). All three alarm RPCs are session-less: the scope check is the only authorization gate, since there is no per-session ownership to enforce.
|
||||||
|
|
||||||
`MxCommandRequest` is special because it multiplexes many MxAccess operations through a single RPC. The resolver inspects the embedded `MxCommandKind` so each operation gets its own scope:
|
`MxCommandRequest` is special because it multiplexes many MxAccess operations through a single RPC. The resolver inspects the embedded `MxCommandKind` so each operation gets its own scope:
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,17 @@ worker prepares the replacement on its STA before swapping it into the existing
|
|||||||
failover consumer and returns a normal `MxCommandReply`; callers must treat only
|
failover consumer and returns a normal `MxCommandReply`; callers must treat only
|
||||||
`protocol_status = OK` as acknowledgement of the new list.
|
`protocol_status = OK` as acknowledgement of the new list.
|
||||||
|
|
||||||
|
`AcknowledgeAlarmRequest` carries the operator's identity: `operator_user`
|
||||||
|
(required), `operator_password` (field 6) and `operator_domain` (field 7). The
|
||||||
|
gateway copies them onto `AcknowledgeAlarmCommand` (`operator_password` = 7) and
|
||||||
|
`AcknowledgeAlarmByNameCommand` (`operator_password` = 9); the worker
|
||||||
|
authenticates the operator and writes `AckMsg` as that user. The fields are
|
||||||
|
additive, so `GatewayProtocolVersion` stays `3`. `operator_password` is a
|
||||||
|
credential and follows the same never-log rule as
|
||||||
|
`AuthenticateUserCommand.verify_user_password`: it must not reach logs, metric
|
||||||
|
labels, `DiagnosticMessage`, `ProtocolStatus.Message`, or exception text. A
|
||||||
|
not-found or not-ackable alarm is reported with `hresult` -1.
|
||||||
|
|
||||||
The command model also includes bulk write/read command kinds:
|
The command model also includes bulk write/read command kinds:
|
||||||
`WriteBulk`, `Write2Bulk`, `WriteSecuredBulk`, `WriteSecured2Bulk`, and
|
`WriteBulk`, `Write2Bulk`, `WriteSecuredBulk`, `WriteSecured2Bulk`, and
|
||||||
`ReadBulk`. They are unary `Invoke` payloads on the same `MxAccessGateway`
|
`ReadBulk`. They are unary `Invoke` payloads on the same `MxAccessGateway`
|
||||||
|
|||||||
+42
-8
@@ -703,14 +703,15 @@ latch in subtag mode.
|
|||||||
|
|
||||||
### Acknowledge via ack-comment write
|
### Acknowledge via ack-comment write
|
||||||
|
|
||||||
In subtag mode, `AcknowledgeAlarm` writes the operator comment to the alarm
|
Both provider modes acknowledge by writing the operator comment to `AckMsg` as
|
||||||
attribute's ack-comment subtag (`Fallback:Subtags:AckComment`). The write
|
the authenticated operator (see "Alarm acknowledgement writes AckMsg as the
|
||||||
performs the native ack in AVEVA. This differs from alarmmgr mode, where
|
authenticated operator" below). In alarm-manager mode the target is
|
||||||
`AlarmAckByName` on `wwAlarmConsumerClass` is called directly. The `AckComment`
|
`<TagName>.AckMsg`; in subtag mode it is the watch-list entry's ack-comment
|
||||||
subtag name is empty by default; configuring it is required for ack to work in
|
subtag (`Fallback:Subtags:AckComment`, default `AckMsg`). The write performs
|
||||||
subtag mode. The exact AVEVA subtag names are not hard-coded — the `Subtags`
|
the ack in AVEVA. An entry with an empty ack-comment subtag is not ackable and
|
||||||
config block exists precisely so names are not guessed without validation
|
the RPC reports `InvalidRequest`. The exact AVEVA subtag names are not
|
||||||
against the live MXAccess attribute set.
|
hard-coded — the `Subtags` config block exists so names are not guessed
|
||||||
|
without validation against the live MXAccess attribute set.
|
||||||
|
|
||||||
### Related documentation
|
### Related documentation
|
||||||
|
|
||||||
@@ -784,6 +785,39 @@ The following items were previously listed here and have since shipped:
|
|||||||
`MxGateway:Sessions:MaxEventSubscribersPerSession`.
|
`MxGateway:Sessions:MaxEventSubscribersPerSession`.
|
||||||
See [Event Subscribers](#event-subscribers) above and [Sessions](./Sessions.md).
|
See [Event Subscribers](#event-subscribers) above and [Sessions](./Sessions.md).
|
||||||
|
|
||||||
|
## Alarm acknowledgement writes AckMsg as the authenticated operator
|
||||||
|
|
||||||
|
**Decision (2026-10-19).** `AcknowledgeAlarm` authenticates the requesting
|
||||||
|
operator with `AuthenticateUser` (`operator_user`, `operator_password`,
|
||||||
|
optional `operator_domain`) and writes the comment to `<TagName>.AckMsg` over
|
||||||
|
MXAccess as that user, on a dedicated `LMXProxyServer` registered as
|
||||||
|
`MxGateway:Alarms:AckClientName`. Both provider modes use this path.
|
||||||
|
|
||||||
|
**Why.** Live tests (2026-10-18/19) showed every alarm-client ack is inert on a
|
||||||
|
headless consumer: wnwrap `AlarmAckByName`/`AlarmAckByGUID`/`AlarmAck`/
|
||||||
|
`AlarmAckSelected`, and the managed `aaAlarmManagedClient` GUID ack, return
|
||||||
|
rc=0 or -1 and change nothing, regardless of security classification,
|
||||||
|
operator name or consumer name. An MXAccess write to `AckMsg` does acknowledge
|
||||||
|
the alarm, and when the write is made under an authenticated user the AVEVA
|
||||||
|
runtime historizes that identity (`Operator`, `DomainName`, `UserFullName`),
|
||||||
|
with the MXAccess client name as `OperatorNode`. The requirement is that the
|
||||||
|
real operator appears in the Historian, as with InTouch, so a shared gateway
|
||||||
|
service account was rejected. Operators have Galaxy accounts, so the
|
||||||
|
per-request credential is workable. See
|
||||||
|
[Alarm Probe Findings](./AlarmProbeFindings.md).
|
||||||
|
|
||||||
|
**Consequences.**
|
||||||
|
|
||||||
|
- `operator_user` is required; `operator_password` may be empty.
|
||||||
|
- `operator_password` is a credential. It is forwarded to the worker over the
|
||||||
|
pipe but never logged: `AcknowledgeAlarm` and `AcknowledgeAlarmByName` are
|
||||||
|
credential-bearing commands and any property named like `password` is
|
||||||
|
redacted.
|
||||||
|
- `Applied` and `Unconfirmed` (write sent, no `OnWriteComplete` in the bounded
|
||||||
|
wait) both report `OK`; clients that need certainty watch for the transition.
|
||||||
|
- The managed ack consumer, the WNAL native ack consumer, the second wnwrap
|
||||||
|
ack-only consumer and the opt-in managed-ack environment flag are removed.
|
||||||
|
|
||||||
## Related Documentation
|
## Related Documentation
|
||||||
|
|
||||||
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
|
- [Gateway Process Detailed Design](./GatewayProcessDesign.md)
|
||||||
|
|||||||
+5
-3
@@ -4,7 +4,7 @@ The diagnostics subsystem provides structured logging, credential redaction, and
|
|||||||
|
|
||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
The subsystem exists to satisfy two security rules from `gateway.md`: never log passwords or raw credential values for `AuthenticateUser`, `WriteSecured`, or related secured operations, and never log full MXAccess values by default. Code paths that touch credentials or tag values must therefore route through `GatewayLogRedactor` rather than emitting them directly.
|
The subsystem exists to satisfy two security rules from `gateway.md`: never log passwords or raw credential values for `AuthenticateUser`, `WriteSecured`, `AcknowledgeAlarm` (its `operator_password`), or related secured operations, and never log full MXAccess values by default. Code paths that touch credentials or tag values must therefore route through `GatewayLogRedactor` rather than emitting them directly.
|
||||||
|
|
||||||
A second goal is parity-test diagnosability. Because MXAccess sessions, workers, correlation ids, and command methods are the units of comparison, every log entry produced inside a request scope must carry those identifiers without each call site having to format them.
|
A second goal is parity-test diagnosability. Because MXAccess sessions, workers, correlation ids, and command methods are the units of comparison, every log entry produced inside a request scope must carry those identifiers without each call site having to format them.
|
||||||
|
|
||||||
@@ -70,7 +70,9 @@ private static readonly HashSet<string> SensitiveCommandMethods = new(StringComp
|
|||||||
{
|
{
|
||||||
"AuthenticateUser",
|
"AuthenticateUser",
|
||||||
"WriteSecured",
|
"WriteSecured",
|
||||||
"WriteSecured2"
|
"WriteSecured2",
|
||||||
|
"AcknowledgeAlarm",
|
||||||
|
"AcknowledgeAlarmByName"
|
||||||
};
|
};
|
||||||
|
|
||||||
public static bool IsCredentialBearingCommand(string? commandMethod)
|
public static bool IsCredentialBearingCommand(string? commandMethod)
|
||||||
@@ -80,7 +82,7 @@ public static bool IsCredentialBearingCommand(string? commandMethod)
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The names match the MXAccess command list in `gateway.md` exactly. `Write` and `Write2` are not in the set because their payloads are tag values, not credentials, and are governed by the `valueLoggingEnabled` flag described below.
|
The names match the MXAccess command list in `gateway.md` exactly. `Write` and `Write2` are not in the set because their payloads are tag values, not credentials, and are governed by the `valueLoggingEnabled` flag described below. `AcknowledgeAlarm` and `AcknowledgeAlarmByName` are in the set because their commands carry the operator's `operator_password`. Independently of the method set, the name-based seam redacts any property whose name contains `password`, so a credential field added to a payload later is redacted without a code change.
|
||||||
|
|
||||||
### API key redaction
|
### API key redaction
|
||||||
|
|
||||||
|
|||||||
@@ -442,17 +442,18 @@ behavior.
|
|||||||
| `MxGateway:Alarms:ReconcileIntervalSeconds` | `30` | How often the monitor re-resolves and, on material change, hot-swaps the subtag fallback watch-list, then reconciles its in-process alarm cache against the worker's authoritative active-alarm snapshot. The acknowledged watch-list fingerprint advances only after an OK worker reply. Floored at 5 seconds. |
|
| `MxGateway:Alarms:ReconcileIntervalSeconds` | `30` | How often the monitor re-resolves and, on material change, hot-swaps the subtag fallback watch-list, then reconciles its in-process alarm cache against the worker's authoritative active-alarm snapshot. The acknowledged watch-list fingerprint advances only after an OK worker reply. Floored at 5 seconds. |
|
||||||
| `MxGateway:Alarms:PollIntervalMilliseconds` | `500` | Cadence at which the worker's STA polls the AVEVA alarm consumer (`GetXmlCurrentAlarms2`) for the active-alarm snapshot the live feed diffs. Must be between `100` and `3600000` (one hour): every poll is a COM call plus an XML parse on the same STA that serves reads and writes, so a tighter cadence starves the command path, while a value above an hour stops being a cadence and silently disables alarm polling. The gateway conveys the value to the worker via the `MXGATEWAY_ALARM_POLL_INTERVAL_MS` environment variable; a missing or out-of-range value leaves the worker on the 500 ms default rather than failing the session. |
|
| `MxGateway:Alarms:PollIntervalMilliseconds` | `500` | Cadence at which the worker's STA polls the AVEVA alarm consumer (`GetXmlCurrentAlarms2`) for the active-alarm snapshot the live feed diffs. Must be between `100` and `3600000` (one hour): every poll is a COM call plus an XML parse on the same STA that serves reads and writes, so a tighter cadence starves the command path, while a value above an hour stops being a cadence and silently disables alarm polling. The gateway conveys the value to the worker via the `MXGATEWAY_ALARM_POLL_INTERVAL_MS` environment variable; a missing or out-of-range value leaves the worker on the 500 ms default rather than failing the session. |
|
||||||
| `MxGateway:Alarms:MaxAlarmsPerFetch` | `1024` | Cap the worker passes to `GetXmlCurrentAlarms2`'s `maxAlmCnt`. Must be between `64` and `65536` — the worker is a 32-bit process that materializes each reply as one BSTR plus a full `XmlDocument`, so an unbounded cap faults the STA with an out-of-memory rather than merely slowing it. It doubles as the **truncation threshold**: a fetch returning exactly this many records is treated as truncated, because the COM API caps its reply with no "more available" flag. On a truncated poll the worker retains the alarms the capped reply could not mention instead of letting their absence read as a clear, and logs a rate-limited `AlarmSnapshotTruncated` warning to its stderr (identifiers and counts only). **Remediation when you see that warning: raise this value** so the steady-state active-alarm count fits inside one fetch. A galaxy permanently above the cap holds stale entries in the snapshot until a sub-cap poll, and loses clear→re-raise cycles that happen entirely out of window (see `docs/DesignDecisions.md`). Conveyed to the worker via the `MXGATEWAY_ALARM_MAX_ALARMS_PER_FETCH` environment variable; a missing or out-of-range value leaves the worker on the 1024 default. |
|
| `MxGateway:Alarms:MaxAlarmsPerFetch` | `1024` | Cap the worker passes to `GetXmlCurrentAlarms2`'s `maxAlmCnt`. Must be between `64` and `65536` — the worker is a 32-bit process that materializes each reply as one BSTR plus a full `XmlDocument`, so an unbounded cap faults the STA with an out-of-memory rather than merely slowing it. It doubles as the **truncation threshold**: a fetch returning exactly this many records is treated as truncated, because the COM API caps its reply with no "more available" flag. On a truncated poll the worker retains the alarms the capped reply could not mention instead of letting their absence read as a clear, and logs a rate-limited `AlarmSnapshotTruncated` warning to its stderr (identifiers and counts only). **Remediation when you see that warning: raise this value** so the steady-state active-alarm count fits inside one fetch. A galaxy permanently above the cap holds stale entries in the snapshot until a sub-cap poll, and loses clear→re-raise cycles that happen entirely out of window (see `docs/DesignDecisions.md`). Conveyed to the worker via the `MXGATEWAY_ALARM_MAX_ALARMS_PER_FETCH` environment variable; a missing or out-of-range value leaves the worker on the 1024 default. |
|
||||||
|
| `MxGateway:Alarms:AckClientName` | `MxGateway` | MXAccess client name the alarm worker's ack writer registers with; the Historian records it as each acknowledgement's OperatorNode. 1-64 printable characters. Conveyed via `MXGATEWAY_ALARM_ACK_CLIENT_NAME`. |
|
||||||
|
|
||||||
The alarm monitor is independent of client sessions: `AcknowledgeAlarm` and
|
The alarm monitor is independent of client sessions: `AcknowledgeAlarm` and
|
||||||
`StreamAlarms` are session-less RPCs served by the monitor.
|
`StreamAlarms` are session-less RPCs served by the monitor.
|
||||||
|
|
||||||
The worker also reads `MXGATEWAY_USE_MANAGED_ALARM_ACK` (inherited from the
|
`AckClientName` is the MXAccess client name the alarm worker's ack writer
|
||||||
gateway process). Setting it to `1` is meant to route GUID-keyed acknowledges
|
registers its dedicated `LMXProxyServer` with. The Historian records it as the
|
||||||
through AVEVA's managed alarm client, but a live test (2026-10-18) showed the
|
OperatorNode of each acknowledgement, so it identifies the gateway as the
|
||||||
path cannot work as built: the managed client fails to register alongside the
|
acknowledging node. The gateway passes it to each worker through the
|
||||||
worker's wnwrap reader and the worker silently falls back to wnwrap. Leave it
|
`MXGATEWAY_ALARM_ACK_CLIENT_NAME` environment variable. Startup validation
|
||||||
unset. It has no `MxGateway:*` option. See
|
rejects an empty value, a value over 64 characters, or one containing control
|
||||||
[AlarmClientDiscovery](./AlarmClientDiscovery.md) ("Opt-in managed GUID ack").
|
characters.
|
||||||
|
|
||||||
### Alarm fallback options
|
### Alarm fallback options
|
||||||
|
|
||||||
@@ -471,7 +472,7 @@ native wnwrap alarm-manager provider and the subtag-monitoring fallback.
|
|||||||
| `MxGateway:Alarms:Fallback:Discovery:ExcludeAttributes` | _(empty)_ | Attribute paths to remove from the merged watch-list (case-insensitive). The exclude is applied after the Repository-derived rows and the explicit `IncludeAttributes` entries are combined, so an exclude that matches an explicit include suppresses it too — excludes win. Ignored when `UseGalaxyRepository` is `false`. |
|
| `MxGateway:Alarms:Fallback:Discovery:ExcludeAttributes` | _(empty)_ | Attribute paths to remove from the merged watch-list (case-insensitive). The exclude is applied after the Repository-derived rows and the explicit `IncludeAttributes` entries are combined, so an exclude that matches an explicit include suppresses it too — excludes win. Ignored when `UseGalaxyRepository` is `false`. |
|
||||||
| `MxGateway:Alarms:Fallback:Subtags:Active` | `InAlarm` | Subtag name for the in-alarm boolean. Confirmed AVEVA `AlarmExtension` field name. |
|
| `MxGateway:Alarms:Fallback:Subtags:Active` | `InAlarm` | Subtag name for the in-alarm boolean. Confirmed AVEVA `AlarmExtension` field name. |
|
||||||
| `MxGateway:Alarms:Fallback:Subtags:Acked` | `Acked` | Subtag name for the acknowledged boolean. Confirmed AVEVA `AlarmExtension` field name. |
|
| `MxGateway:Alarms:Fallback:Subtags:Acked` | `Acked` | Subtag name for the acknowledged boolean. Confirmed AVEVA `AlarmExtension` field name. |
|
||||||
| `MxGateway:Alarms:Fallback:Subtags:AckComment` | `AckMsg` | Subtag name for the acknowledgement comment write target. Writing this subtag performs the acknowledge in AVEVA. Confirmed AVEVA `AlarmExtension` field name. When empty, the ack-comment write path is disabled. |
|
| `MxGateway:Alarms:Fallback:Subtags:AckComment` | `AckMsg` | Subtag name for the acknowledgement comment write target. Writing this subtag, as the authenticated operator, performs the acknowledge in AVEVA. Confirmed AVEVA `AlarmExtension` field name. When empty, the alarm is not ackable and the RPC returns `InvalidRequest`. |
|
||||||
| `MxGateway:Alarms:Fallback:Subtags:Priority` | `Priority` | Subtag name for the alarm priority / severity value. Confirmed AVEVA `AlarmExtension` field name. |
|
| `MxGateway:Alarms:Fallback:Subtags:Priority` | `Priority` | Subtag name for the alarm priority / severity value. Confirmed AVEVA `AlarmExtension` field name. |
|
||||||
|
|
||||||
Validation rules:
|
Validation rules:
|
||||||
|
|||||||
+18
-2
@@ -96,9 +96,24 @@ Selected control commands have additional public behavior beyond their protobuf
|
|||||||
|
|
||||||
### `AcknowledgeAlarm`
|
### `AcknowledgeAlarm`
|
||||||
|
|
||||||
`AcknowledgeAlarm` is a unary, **session-less** RPC that acknowledges a single alarm. The handler validates `alarm_full_reference` inline (it does not run through `MxAccessGrpcRequestValidator`) and delegates to `IGatewayAlarmService.AcknowledgeAsync`. The always-on `GatewayAlarmMonitor` routes the ack over its own gateway-managed worker session — clients no longer open a session to acknowledge an alarm. A reference that parses as a canonical GUID forwards to `AcknowledgeAlarmCommand`; a `Provider!Group.Tag` reference forwards to `AcknowledgeAlarmByNameCommand`.
|
`AcknowledgeAlarm` is a unary, **session-less** RPC that acknowledges a single alarm. The handler validates `alarm_full_reference` and `operator_user` inline (it does not run through `MxAccessGrpcRequestValidator`) and delegates to `IGatewayAlarmService.AcknowledgeAsync`. The always-on `GatewayAlarmMonitor` routes the ack over its own gateway-managed worker session — clients no longer open a session to acknowledge an alarm. A reference that parses as a canonical GUID forwards to `AcknowledgeAlarmCommand`; a `Provider!Group.Tag` reference forwards to `AcknowledgeAlarmByNameCommand`.
|
||||||
|
|
||||||
An `OK` response means the alarm provider accepted the acknowledgement, not that it applied it: the gateway forwards the ack and reports what the provider returned, in keeping with the parity rule. On galaxies whose alarmed attributes carry a non-free-access security classification the by-name ack is accepted and inert — the underlying `AlarmAckByName` conveys an operator name rather than an authenticated identity, observed on the probe rig with the mechanism inferred rather than confirmed — so a client that needs to confirm an acknowledgement should watch for the resulting transition on `StreamAlarms` instead of treating `OK` as proof. See [Alarm Probe Findings](./AlarmProbeFindings.md).
|
Request fields that identify the acknowledging operator:
|
||||||
|
|
||||||
|
- `operator_user` (required): the Galaxy account name. An empty value is rejected with `InvalidArgument` so the real operator is always recorded.
|
||||||
|
- `operator_password` (field 6): the operator's Galaxy password. May be empty because Galaxy accounts can have empty passwords. It is a credential and is never logged.
|
||||||
|
- `operator_domain` (field 7): optional. When set, the worker authenticates `domain\user`; when empty it authenticates `user`.
|
||||||
|
|
||||||
|
The worker calls `AuthenticateUser` as the operator, then writes `operator_comment` to `<TagName>.AckMsg` (subtag mode: the configured ack-comment subtag) on a dedicated MXAccess server registered as `MxGateway:Alarms:AckClientName`, and waits a bounded time for `OnWriteComplete`. The AVEVA runtime treats that write as the acknowledgement, and the Historian (`Runtime.dbo.v_AlarmEventHistory2`) records the operator name, domain and full name from the authenticated account, with the ack client name as `OperatorNode`. The alarm-client ack calls (`AlarmAckByName`, `AlarmAckByGUID`) are not used because a headless consumer's acks are accepted and dropped; see [Alarm Probe Findings](./AlarmProbeFindings.md).
|
||||||
|
|
||||||
|
| Worker outcome | Response status | `hresult` |
|
||||||
|
|---|---|---|
|
||||||
|
| Write applied, or sent but no `OnWriteComplete` within the wait (unconfirmed, with a diagnostic) | `OK` | `0` |
|
||||||
|
| Unknown alarm, or no ack target (non-Galaxy provider, no ack-comment subtag) | `INVALID_REQUEST` | `-1` |
|
||||||
|
| `AuthenticateUser` failed | `MXACCESS_FAILURE` | the `AuthenticateUser` HRESULT |
|
||||||
|
| Write rejected (`OnWriteComplete` non-OK) | `MXACCESS_FAILURE` | the status `detail` |
|
||||||
|
|
||||||
|
`reply.Statuses` carries the `OnWriteComplete` `MxStatusProxy` rows when they were observed. Because an unconfirmed write also returns `OK`, a client that must be certain should watch for the resulting transition on `StreamAlarms`. `AcknowledgeAlarm` stays under the `invoke:write` scope; see [Authorization](./Authorization.md).
|
||||||
|
|
||||||
### `StreamAlarms`
|
### `StreamAlarms`
|
||||||
|
|
||||||
@@ -217,6 +232,7 @@ receive this event directly.
|
|||||||
| `StreamEvents` | `session_id` must be non-empty. | `InvalidArgument` |
|
| `StreamEvents` | `session_id` must be non-empty. | `InvalidArgument` |
|
||||||
| `Invoke` | `session_id` non-empty, `command` present, `kind` not `Unspecified`, payload oneof must match `kind`. | `InvalidArgument` |
|
| `Invoke` | `session_id` non-empty, `command` present, `kind` not `Unspecified`, payload oneof must match `kind`. | `InvalidArgument` |
|
||||||
| `AcknowledgeAlarm` | `alarm_full_reference` must be non-empty. Validated inline in the handler, not by `MxAccessGrpcRequestValidator`. | `InvalidArgument` |
|
| `AcknowledgeAlarm` | `alarm_full_reference` must be non-empty. Validated inline in the handler, not by `MxAccessGrpcRequestValidator`. | `InvalidArgument` |
|
||||||
|
| `AcknowledgeAlarm` | `operator_user` must be non-empty. Validated inline in the handler. | `InvalidArgument` |
|
||||||
| `StreamAlarms` | No required fields — `alarm_filter_prefix` is optional. | — |
|
| `StreamAlarms` | No required fields — `alarm_filter_prefix` is optional. | — |
|
||||||
| `QueryActiveAlarms` | No required fields — `alarm_filter_prefix` is optional. Not routed through `MxAccessGrpcRequestValidator`; the handler only null-checks the request. | — |
|
| `QueryActiveAlarms` | No required fields — `alarm_filter_prefix` is optional. Not routed through `MxAccessGrpcRequestValidator`; the handler only null-checks the request. | — |
|
||||||
|
|
||||||
|
|||||||
+22
-18
@@ -140,8 +140,9 @@ one gateway-owned worker session subscribes the configured AVEVA alarm
|
|||||||
provider, caches the active-alarm set (reconciled periodically against the
|
provider, caches the active-alarm set (reconciled periodically against the
|
||||||
worker's snapshot), and fans it out to every client through the session-less
|
worker's snapshot), and fans it out to every client through the session-less
|
||||||
`StreamAlarms` RPC — the stream opens with the current active-alarm snapshot,
|
`StreamAlarms` RPC — the stream opens with the current active-alarm snapshot,
|
||||||
then streams live transitions. `AcknowledgeAlarm` is session-less and routes
|
then streams live transitions. `AcknowledgeAlarm` is session-less, routes
|
||||||
through the monitor. Clients never open a worker session to see alarms, and
|
through the monitor, and carries the operator's credentials (see
|
||||||
|
"Acknowledge" below). Clients never open a worker session to see alarms, and
|
||||||
alarm monitoring is independent of client lifecycle; the monitor re-opens its
|
alarm monitoring is independent of client lifecycle; the monitor re-opens its
|
||||||
session if the worker faults. Gated by `MxGateway:Alarms:Enabled` — see
|
session if the worker faults. Gated by `MxGateway:Alarms:Enabled` — see
|
||||||
`docs/DesignDecisions.md` for why this reverses the v1 single-subscriber rule
|
`docs/DesignDecisions.md` for why this reverses the v1 single-subscriber rule
|
||||||
@@ -221,20 +222,22 @@ forwards only events the worker emits and never synthesizes transitions
|
|||||||
itself. The synthesis rules are documented in
|
itself. The synthesis rules are documented in
|
||||||
`docs/AlarmClientDiscovery.md`.
|
`docs/AlarmClientDiscovery.md`.
|
||||||
|
|
||||||
**Acknowledge in subtag mode:** the ack-by-name path writes the operator
|
**Acknowledge:** the gateway acknowledges an alarm by writing the operator's
|
||||||
comment to the alarm attribute's ack-comment subtag. The write performs the
|
comment to the alarm attribute's `AckMsg` (subtag mode: the configured
|
||||||
ack. If the attribute has no writable ack-comment subtag configured, the RPC
|
ack-comment subtag) over MXAccess, after `AuthenticateUser` as the requesting
|
||||||
returns `FailedPrecondition`. In alarm-manager mode, `AlarmAckByName` is
|
operator (`operator_user`, `operator_password`, optional `operator_domain`).
|
||||||
used as before.
|
The AVEVA runtime treats that write as the acknowledgement and historizes it
|
||||||
|
with the authenticated account's name, domain and full name; the Historian's
|
||||||
**Acceptance is not application:** the ack is forwarded to the provider, and a
|
OperatorNode is the ack writer's MXAccess client name
|
||||||
successful return means the provider accepted the call, not that the
|
(`MxGateway:Alarms:AckClientName`). The worker waits a bounded time for
|
||||||
acknowledgement was applied. On galaxies whose alarmed attributes carry a
|
`OnWriteComplete`: an OK status is reported as success, a non-OK status as
|
||||||
non-free-access security classification, `AlarmAckByName` returns `rc=0` and the
|
`MxaccessFailure` with the status detail, and no completion as success with a
|
||||||
alarm stays `UNACK_ALM` — it carries an operator name, not an authenticated
|
diagnostic noting the write was sent but unconfirmed. An unknown alarm or one
|
||||||
identity. (Observed on the probe rig; the mechanism is inferred.) Confirm an ack
|
with no ack target (non-Galaxy provider, no ack-comment subtag) is reported as
|
||||||
by the resulting transition, never by the return code. See
|
`InvalidRequest` with `hresult` -1. Alarm-client ack calls
|
||||||
`docs/AlarmProbeFindings.md`.
|
(`AlarmAckByName`/`AlarmAckByGUID`, the managed `AlarmClient`, WNAL) are not
|
||||||
|
used: live tests showed a headless consumer's acks are accepted and silently
|
||||||
|
dropped (see `docs/AlarmProbeFindings.md`).
|
||||||
|
|
||||||
**Degraded state visibility:** every subtag-mode transition carries
|
**Degraded state visibility:** every subtag-mode transition carries
|
||||||
`degraded = true` and `source_provider = ALARM_PROVIDER_MODE_SUBTAG` on the
|
`degraded = true` and `source_provider = ALARM_PROVIDER_MODE_SUBTAG` on the
|
||||||
@@ -1028,8 +1031,9 @@ Internal worker IPC:
|
|||||||
- gateway validates worker executable path and version,
|
- gateway validates worker executable path and version,
|
||||||
- no secrets in command line when avoidable.
|
- no secrets in command line when avoidable.
|
||||||
|
|
||||||
Credential-sensitive commands such as `AuthenticateUser` and `WriteSecured`
|
Credential-sensitive commands such as `AuthenticateUser`, `WriteSecured` and
|
||||||
must not log passwords or raw credential values.
|
`AcknowledgeAlarm` (its `operator_password`) must not log passwords or raw
|
||||||
|
credential values.
|
||||||
|
|
||||||
## Observability
|
## Observability
|
||||||
|
|
||||||
|
|||||||
@@ -88,7 +88,8 @@
|
|||||||
"DefaultArea": "",
|
"DefaultArea": "",
|
||||||
"ReconcileIntervalSeconds": 30,
|
"ReconcileIntervalSeconds": 30,
|
||||||
"PollIntervalMilliseconds": 500,
|
"PollIntervalMilliseconds": 500,
|
||||||
"MaxAlarmsPerFetch": 1024
|
"MaxAlarmsPerFetch": 1024,
|
||||||
|
"AckClientName": "MxGateway"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,14 +202,14 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
|||||||
|
|
||||||
// Use the IwwAlarmConsumer (v1) prefix-named methods for the
|
// Use the IwwAlarmConsumer (v1) prefix-named methods for the
|
||||||
// lifecycle. Empirically (live dev-rig 2026-05-01, reconfirmed
|
// lifecycle. Empirically (live dev-rig 2026-05-01, reconfirmed
|
||||||
// 2026-08-18) this is the only path that lets AlarmAckByName
|
// 2026-08-18) the v1 path is the one that leaves the consumer in
|
||||||
// return rc=0 afterwards. The v2 Initialize/Register/Subscribe
|
// a usable state: the v2 Initialize/Register/Subscribe methods on
|
||||||
// methods on the class succeed (return 0) but acks against that
|
// the class succeed (return 0) but later calls against that
|
||||||
// consumer state return -55. The v1 prefix path is what
|
// consumer state return -55. The v1 prefix path is what
|
||||||
// WIN-911-style code uses against the same wnwrap library.
|
// WIN-911-style code uses against the same wnwrap library. This
|
||||||
// Note rc=0 means the call was
|
// consumer only reads the alarm snapshot; acknowledgements go
|
||||||
// accepted, not that an acknowledgement was applied — see
|
// through AlarmAckWriter, not through wnwrap (see
|
||||||
// docs/AlarmProbeFindings.md. Acks now go through AlarmAckWriter.
|
// docs/AlarmProbeFindings.md).
|
||||||
int init = com.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName);
|
int init = com.IwwAlarmConsumer_InitializeConsumer(DefaultApplicationName);
|
||||||
if (init != 0)
|
if (init != 0)
|
||||||
{
|
{
|
||||||
@@ -615,10 +615,10 @@ public sealed class WnWrapAlarmConsumer : IMxAccessAlarmConsumer
|
|||||||
/// place under its original GUID and the re-raise replaced it with a
|
/// place under its original GUID and the re-raise replaced it with a
|
||||||
/// freshly minted one. That is the intended reading — the old
|
/// freshly minted one. That is the intended reading — the old
|
||||||
/// instance ended and a new one began — so the diff emits a Clear
|
/// instance ended and a new one began — so the diff emits a Clear
|
||||||
/// plus a Raise, which is what happened. The acknowledge leg remains
|
/// plus a Raise, which is what happened. Every alarm-client ack surface
|
||||||
/// assumed rather than observed: on the dev rig every ack surface is
|
/// is inert on the dev rig (it returns 0 and nothing moves), so
|
||||||
/// inert (<c>AlarmAckByName</c> returns 0 and nothing moves), so no
|
/// acknowledgements go through the AckMsg writer; an AckMsg write
|
||||||
/// record can be driven into an acknowledged state — see
|
/// moves the record to <c>ACK_ALM</c> (observed 2026-10-19) — see
|
||||||
/// <c>docs/AlarmProbeFindings.md</c>.
|
/// <c>docs/AlarmProbeFindings.md</c>.
|
||||||
/// </para>
|
/// </para>
|
||||||
/// </remarks>
|
/// </remarks>
|
||||||
|
|||||||
Reference in New Issue
Block a user