Acknowledge alarms as the authenticated operator (AckMsg write) #162

Closed
dohertj2 wants to merge 0 commits from feat/ackmsg-as-operator into main
Owner

Acknowledges an alarm as the operator who requested it, so the Historian events table records the real operator, the way InTouch does.

The worker authenticates the operator with AuthenticateUser, then writes the ack comment to <TAGNAME>.AckMsg over MXAccess. It does this on its own LMXProxyServer, registered under MxGateway:Alarms:AckClientName (default MxGateway); the Historian records that name as OperatorNode. The alarm-client ack paths (wnwrap AlarmAckByName/ByGUID, the managed aaAlarmManagedClient, WNAL) are inert when run headless (see docs/AlarmProbeFindings.md, "Fourth attempt"), so this PR removes them.

Changes

  • Contract: AcknowledgeAlarmRequest.operator_password = 6, operator_domain = 7. Worker ack commands also carry the password. GatewayProtocolVersion stays at 3.
  • Worker:
    • New AlarmAckWriter: authenticate, AddItem+Advise, Write, then a bounded pumped wait for OnWriteComplete. It keeps an LRU item cache, rebuilds the item after an unconfirmed write, tears down and re-registers on RPC disconnect, and classifies interop failures by HRESULT.
    • Consumers now only resolve the AckMsg item; a non-Galaxy provider is NotAckable.
    • The wnwrap ACK_* XML fields are parsed and attributed on Acknowledge transitions.
    • The managed and WNAL ack path and the second wnwrap ack consumer are deleted.
  • Gateway: forwards the credentials. operator_user is required (InvalidArgument otherwise). Ack commands are now treated as credential-bearing for log redaction, and any property named like password is redacted. New AckClientName option, passed to the worker by env var.
  • Clients (.NET, Python, Go, Rust, Java): request fields, password redaction on ack errors, and CLI flags --operator-domain, --operator-password, --operator-password-env (default MXGATEWAY_OPERATOR_PASSWORD).
  • Docs: Grpc.md (status mapping, upgrade note), DesignDecisions.md, GatewayConfiguration.md, Diagnostics.md, Authorization.md (the credential-oracle trade-off of keeping ack under invoke:write), AlarmProbeFindings.md, CLAUDE.md.

Status mapping

Outcome Status hresult
Applied / unconfirmed OK 0
Unknown alarm / non-Galaxy provider INVALID_REQUEST -1
Authentication failed MXACCESS_FAILURE the AuthenticateUser HRESULT (wrong password = 0x80070057)
Write rejected MXACCESS_FAILURE the HRESULT, or the OnWriteComplete detail

Upgrade impact

Callers must send operator_user and the operator's Galaxy password. OtOpcUa's GatewayGalaxyAlarmAcknowledger (Runtime/GatewayGalaxyAlarmAcknowledger.cs:50) sends no password. Update it before this gateway is deployed anywhere OtOpcUa acks alarms.

Verification

  • macOS: gateway suite 1216/1216. .NET client 135 pass. Go, Python (171), Rust (111), and Java test + checkGeneratedClean all green.
  • windev worker CI: SUCCESS, 574 pass / 0 fail.
  • Live on windev (Server + Worker at e7f5662 deployed, then rolled back):
    • An RPC ack as Administrator returns OK / hresult 0. .Acked=True, .AckMsg = the comment.
    • Historian ACK_ALM row: Operator=Administrator, DomainName=ArchestrA, UserFullName=Administrator, OperatorNode=MxGateway, Description=<comment>.
    • Wrong password returns MXACCESS_FAILURE "Operator authentication failed"; the alarm stays unacked, no ack row is written, and the password has 0 hits in the gateway log, stdout and stderr.
    • InTouch provider returns INVALID_REQUEST. Empty user returns InvalidArgument.
  • Not driven live: the GUID arm. The public RPCs expose only full references, so it is covered by unit tests only.

Deploy notes found on windev (not changed by this PR)

  • The current Secrets library rejects a relative Secrets:SqlitePath, or one inside the content root. The old live appsettings.json used mxgateway-secrets.db, so the test deploy moved the store to C:\publish\mxaccessgw\secrets\.
  • NU1903 (GHSA-846f-f764-76mx) on transitive System.Security.Cryptography.Pkcs 9.0.4 now fails dotnet restore on windev because of TreatWarningsAsErrors. This also affects main.

https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT

Acknowledges an alarm as the operator who requested it, so the Historian events table records the real operator, the way InTouch does. The worker authenticates the operator with `AuthenticateUser`, then writes the ack comment to `<TAGNAME>.AckMsg` over MXAccess. It does this on its own `LMXProxyServer`, registered under `MxGateway:Alarms:AckClientName` (default `MxGateway`); the Historian records that name as `OperatorNode`. The alarm-client ack paths (wnwrap `AlarmAckByName`/`ByGUID`, the managed `aaAlarmManagedClient`, WNAL) are inert when run headless (see `docs/AlarmProbeFindings.md`, "Fourth attempt"), so this PR removes them. ## Changes - **Contract:** `AcknowledgeAlarmRequest.operator_password = 6`, `operator_domain = 7`. Worker ack commands also carry the password. `GatewayProtocolVersion` stays at 3. - **Worker:** - New `AlarmAckWriter`: authenticate, AddItem+Advise, Write, then a bounded pumped wait for `OnWriteComplete`. It keeps an LRU item cache, rebuilds the item after an unconfirmed write, tears down and re-registers on RPC disconnect, and classifies interop failures by HRESULT. - Consumers now only resolve the AckMsg item; a non-Galaxy provider is NotAckable. - The wnwrap `ACK_*` XML fields are parsed and attributed on Acknowledge transitions. - The managed and WNAL ack path and the second wnwrap ack consumer are deleted. - **Gateway:** forwards the credentials. `operator_user` is required (InvalidArgument otherwise). Ack commands are now treated as credential-bearing for log redaction, and any property named like `password` is redacted. New `AckClientName` option, passed to the worker by env var. - **Clients (.NET, Python, Go, Rust, Java):** request fields, password redaction on ack errors, and CLI flags `--operator-domain`, `--operator-password`, `--operator-password-env` (default `MXGATEWAY_OPERATOR_PASSWORD`). - **Docs:** `Grpc.md` (status mapping, upgrade note), `DesignDecisions.md`, `GatewayConfiguration.md`, `Diagnostics.md`, `Authorization.md` (the credential-oracle trade-off of keeping ack under `invoke:write`), `AlarmProbeFindings.md`, `CLAUDE.md`. ## Status mapping | Outcome | Status | hresult | |---|---|---| | Applied / unconfirmed | OK | 0 | | Unknown alarm / non-Galaxy provider | INVALID_REQUEST | -1 | | Authentication failed | MXACCESS_FAILURE | the AuthenticateUser HRESULT (wrong password = 0x80070057) | | Write rejected | MXACCESS_FAILURE | the HRESULT, or the OnWriteComplete detail | ## Upgrade impact Callers must send `operator_user` and the operator's Galaxy password. **OtOpcUa's `GatewayGalaxyAlarmAcknowledger` (`Runtime/GatewayGalaxyAlarmAcknowledger.cs:50`) sends no password.** Update it before this gateway is deployed anywhere OtOpcUa acks alarms. ## Verification - **macOS:** gateway suite 1216/1216. .NET client 135 pass. Go, Python (171), Rust (111), and Java `test` + `checkGeneratedClean` all green. - **windev worker CI:** `SUCCESS`, 574 pass / 0 fail. - **Live on windev** (Server + Worker at e7f5662 deployed, then rolled back): - An RPC ack as `Administrator` returns OK / hresult 0. `.Acked=True`, `.AckMsg` = the comment. - Historian `ACK_ALM` row: `Operator=Administrator, DomainName=ArchestrA, UserFullName=Administrator, OperatorNode=MxGateway, Description=<comment>`. - Wrong password returns MXACCESS_FAILURE "Operator authentication failed"; the alarm stays unacked, no ack row is written, and the password has 0 hits in the gateway log, stdout and stderr. - InTouch provider returns INVALID_REQUEST. Empty user returns InvalidArgument. - **Not driven live:** the GUID arm. The public RPCs expose only full references, so it is covered by unit tests only. ## Deploy notes found on windev (not changed by this PR) - The current Secrets library rejects a relative `Secrets:SqlitePath`, or one inside the content root. The old live `appsettings.json` used `mxgateway-secrets.db`, so the test deploy moved the store to `C:\publish\mxaccessgw\secrets\`. - `NU1903` (GHSA-846f-f764-76mx) on transitive `System.Security.Cryptography.Pkcs` 9.0.4 now fails `dotnet restore` on windev because of `TreatWarningsAsErrors`. This also affects `main`. https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
dohertj2 added 21 commits 2026-10-19 20:09:34 -04:00
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
feat(worker): attribute alarm ack transitions from wnwrap ACK fields
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 2m7s
ci / java (push) Failing after 2m17s
ci / portable (push) Failing after 2m23s
64dc1215fd
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
feat(worker): acknowledge alarms via authenticated AckMsg write
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Failing after 56s
ci / java (push) Failing after 2m17s
ci / portable (push) Failing after 2m23s
76db822df3
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
fix(worker): disambiguate MxStatusCategory in AlarmAckWriter
ci / nightly-windev (push) Has been skipped
ci / java (push) Failing after 2m25s
ci / portable (push) Failing after 2m26s
ci / windows-x86 (push) Successful in 2m27s
7900df3dce
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
fix(worker): handle AckMsg add/advise/write COM failures and server death
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m55s
ci / java (push) Failing after 2m2s
ci / portable (push) Failing after 2m17s
8199553734
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
IMxAccessAlarmConsumer.ResolveAckTarget/ResolveAckTargetByName replace AcknowledgeByGuid/AcknowledgeByName. wnwrap maps Galaxy-provider alarms to <tag>.AckMsg and drops its inert parallel ack consumer; subtag resolves to the target's AckCommentSubtag and no longer advises or writes it; ISubtagAlarmSource.Write is removed. Does not build alone: AlarmDispatcher moves to the new API in the next commit.

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
feat(worker): route alarm acks through the operator-authenticated AckMsg writer
ci / nightly-windev (push) Has been skipped
ci / java (push) Failing after 2m5s
ci / windows-x86 (push) Successful in 2m10s
ci / portable (push) Failing after 2m17s
d6db0ff40d
AlarmCommandHandler -> AlarmDispatcher -> IAlarmAckWriter: the dispatcher resolves the ack item through the consumer and the writer authenticates the operator and writes the comment. The executor builds AlarmAckOperator from operator_user/domain/password, maps AlarmAckOutcome to the reply (Applied/Unconfirmed Ok, AlarmNotFound/NotAckable InvalidRequest, AuthenticationFailed/WriteRejected MxaccessFailure) and reports escaping exceptions by type and HRESULT only, so operator_password cannot leak. Removes the inert managed/WNAL alarm-client ack path.

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
docs: address Task 12 review (supersede stale ack steps, outcome rows)
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 2m10s
ci / windows-x86 (push) Successful in 2m11s
ci / portable (push) Failing after 2m18s
1998ee3e20
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
fix: address final ackmsg-as-operator review findings
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 2m0s
ci / windows-x86 (push) Successful in 2m11s
ci / portable (push) Failing after 2m21s
e7f56622c1
- AlarmAckWriter: classify AuthenticateUser and AddItem/Advise/Write
  failures by HRESULT for any interop exception (ArgumentException for
  E_INVALIDARG, UnauthorizedAccessException, COMException); server-gone
  HRESULTs at AuthenticateUser tear down the registration and report
  WriteRejected.
- Bound the ack item cache with a 256-entry LRU (eviction best-effort
  UnAdvise+RemoveItem).
- After an Unconfirmed ack, drop and re-add the item before the next
  write so a late OnWriteComplete on the old handle is not credited.
- Pass StaRuntime.PumpPendingMessages to the ack writer through a
  pump-aware alarm handler factory so long completion waits refresh the
  STA watchdog heartbeat.
- Rust CLI: --operator is required for acknowledge-alarm.
- Docs: full ack status table, credential-oracle trade-off, upgrade note.

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
docs: record gateway-level live verification of AckMsg-as-operator acks
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m21s
ci / nightly-windev (pull_request) Has been skipped
ci / java (push) Successful in 2m3s
ci / portable (push) Failing after 2m19s
ci / windows-x86 (pull_request) Successful in 1m19s
ci / java (pull_request) Successful in 2m2s
ci / portable (pull_request) Failing after 2m17s
3e988ede88
Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
dohertj2 added 3 commits 2026-10-20 07:15:17 -04:00
Resolve the operator user, password and comment before the interop try
blocks, and match ArgumentException as an MXAccess failure only when it
carries E_INVALIDARG and is not ArgumentNullException or
ArgumentOutOfRangeException. A programming error inside the try blocks
now propagates instead of being reported as an authentication or write
failure.

Fixes #164

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Add MxAccessWriteCompletionCache.Remove and RemoveServer and call them
from AlarmAckWriter when it drops an item (dirty re-add, LRU eviction,
failed AddItem/Advise/Write) and when it tears down its registration, so
the cache no longer keeps one entry per dropped handle for the life of
the worker.

Fixes #165

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
docs(worker): record live item-handle-reuse probe for ack writer
ci / windows-x86 (push) Successful in 1m29s
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 2m47s
ci / nightly-windev (pull_request) Has been skipped
ci / portable (push) Failing after 3m1s
ci / portable (pull_request) Failing after 1m46s
ci / windows-x86 (pull_request) Successful in 1m24s
ci / java (pull_request) Successful in 2m36s
bd82036d5f
Add the Skip-gated ItemHandleReuseProbeTests probe and its windev result
(2026-10-20): within one MXAccess registration, item handles increase
monotonically and are never reused across remove/re-add, so the ack
writer's Unconfirmed drop-and-re-add mitigation holds and no per-handle
generation counter is needed. Numbering restarts only on a new
registration, which teardown already isolates. Document the guarantee at
the Dirty re-add and in AlarmProbeFindings.md.

Fixes #166

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
dohertj2 added 1 commit 2026-10-20 07:28:29 -04:00
fix(worker): classify ack interop failures at the COM call; ignore late completions
ci / nightly-windev (push) Has been skipped
ci / windows-x86 (push) Successful in 1m24s
ci / java (push) Successful in 2m32s
ci / nightly-windev (pull_request) Has been skipped
ci / java (pull_request) Successful in 2m38s
ci / portable (push) Failing after 2m52s
ci / portable (pull_request) Failing after 2m53s
ci / windows-x86 (pull_request) Successful in 8m8s
463748e395
Address review of the #164-#166 fixes:

- The HRESULT check could not tell interop from programming errors
  (a managed ArgumentException already defaults to 0x80070057). Wrap each
  MXAccess call (AuthenticateUser, AddItem, Advise, Write) in a narrow
  filter that converts an interop failure to a private marker exception,
  and catch only that marker, so exceptions from the writer's own code
  (argument checks, caching, the pump) propagate.
- A late OnWriteComplete for a dropped handle recreated the cache entry
  that DropItem removed. The writer now records completions only for item
  handles it holds on its live registration.
- The item-handle-reuse probe now asserts that handles within one
  registration strictly increase, so a rerun is a regression check.

Refs #164, #165, #166

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
dohertj2 added 1 commit 2026-10-20 07:28:50 -04:00
Merge branch 'main' into feat/ackmsg-as-operator
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 2m7s
ci / java (pull_request) Successful in 2m18s
ci / nightly-windev (pull_request) Has been skipped
ci / portable (push) Failing after 4m47s
ci / windows-x86 (pull_request) Successful in 2m40s
ci / portable (pull_request) Failing after 4m43s
ci / windows-x86 (push) Successful in 5m38s
02228b2717
Picks up the System.Security.Cryptography.Pkcs 10.0.13 pin (NU1903).

Claude-Session: https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
Author
Owner

Merged into main as 488649c (merge of 02228b2, which windev verified in live mode: Worker.Tests 585/0, gateway 1216/0, live MXAccess 8/0). Closing by hand because the repo does not allow the manually-merged style.

https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT

Merged into main as 488649c (merge of 02228b2, which windev verified in live mode: Worker.Tests 585/0, gateway 1216/0, live MXAccess 8/0). Closing by hand because the repo does not allow the manually-merged style. https://claude.ai/code/session_016J6isfcgqy4SB2RK3ZvFvT
dohertj2 closed this pull request 2026-10-20 08:53:00 -04:00
Some checks are pending
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 2m7s
ci / java (pull_request) Successful in 2m18s
ci / nightly-windev (pull_request) Has been skipped
ci / portable (push) Failing after 4m47s
ci / windows-x86 (pull_request) Successful in 2m40s
ci / portable (pull_request) Failing after 4m43s
ci / windows-x86 (push) Successful in 5m38s

Pull request closed

Sign in to join this conversation.