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

Open
dohertj2 wants to merge 21 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
Some checks are pending
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
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/ackmsg-as-operator:feat/ackmsg-as-operator
git checkout feat/ackmsg-as-operator
Sign in to join this conversation.