glauth.md's OPC-perm group table and pre-provisioned-users table described a directory that no longer exists: all five OPC-perm gids were off by 100 (5501-5505 vs actual 5601-5605), and the user table listed readonly/writeop/writetune/writeconfig/alarmack accounts that scadaproj's shared GLAuth (config.toml) never provisioned. The real per-role testers are opc-readonly/opc-writeop/opc-writetune/opc-writeconfig/opc-alarmack (renamed to avoid a cn= case-collision with their group), and every dev user's password is "password" per config.toml's own comment and passsha256 (not admin123). admin itself is no longer a member of any OPC-perm group in the shared directory (only GwAdmin + OtOpcUa-Admins) - multi-role is the actual "covers every gw capability" account now. The wrong admin123 default had propagated into MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_PASSWORD (docs/GatewayTesting.md) and the WorkerLiveMxAccessSmokeTests.cs literal/comment attributing the rig's GLAuth-bridged ArchestrA identity - both corrected to password. Also made the WriteSecured parity test log (never assert) whether AuthenticateUser succeeded or was rejected: MXAccess parity means a bad credential is a legitimate failure reply, so it must not fail the test, but a stale rig credential was silently degrading the nightly live smoke's WriteSecured leg from an authenticated round-trip to a bare failure-reply-shape check with no visible signal. Verified: dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx succeeds (IntegrationTests builds cleanly on macOS, net10.0, no COM/x86 dependency). Flips Batch B (TST-31) to Done in the cycle-3 remediation tracker.
11 KiB
MxAccessGateway — Remediation Plan & Tracking (2026-08-19 review)
Source review: ../00-overall.md (HEAD 8b3e3b3, committed as f4919ba). Generated 2026-08-20.
Tracks the 34 cycle-3 IDs (GWC-31+, WRK-32+, IPC-33+, SEC-37+, CLI-46+, TST-31+) plus the tracker-housekeeping actions the overall asked for. Cycle-1/2 trackers remain the record for their own IDs.
Status legend: Not started · In progress · Done · Blocked (external) · Deferred (P2) · N/A.
Execution model
This plan is executed as seven parallel batches, each by one subagent working in an isolated git worktree on its own fix/c3-<batch> branch, merged back into main by the coordinator in the order listed (lowest conflict surface first). Every batch must: build the affected component(s) on macOS (NonWindows.slnx — the x86 worker cannot build here), run only the filtered tests it touches, update docs in the same commit, and cite finding IDs in commit messages (fix(IPC-34): …). Worker-side changes (batches E, F) are verified afterwards on windev via windev-worker-ci.ps1 -Mode test.
Model assignment rationale: opus for batches that change concurrency/authorization logic or require reasoning across several interacting files with regression tests; sonnet for contained, well-specified edits with a clear recipe (version bumps, doc reconciliation, a two-line worker fix); haiku for none — every batch either touches code invariants or needs judgement about stale prose, where the cost of a wrong mechanical sweep exceeds the saving.
| Batch | Branch | Findings | Model | Why this model | Status |
|---|---|---|---|---|---|
| A | fix/c3-client-versions |
CLI-46 | sonnet | Mechanical five-client bump + README note + one script guard; recipe is fully specified | Not started |
| B | fix/c3-glauth-doc |
TST-31 | sonnet | Reconcile a table against one TOML source of truth, re-derive one default, change an assert to a log | Done |
| C | fix/c3-tracker-hygiene |
GWC-31 + housekeeping (close GWC-16/IPC-08/IPC-31, refresh TST-06/TST-17, register WRK-31) | sonnet | Cross-file comment/ID sweep needs care about what each ID means, but no logic changes | Not started |
| D | fix/c3-dashboard-acl |
SEC-40, SEC-41, GWC-38 | opus | Authorization semantics, fail-closed decisions, claims re-resolution; each needs a regression test | Not started |
| E | fix/c3-ipc-boundaries |
IPC-33, IPC-39, IPC-34, IPC-35 | opus | Changes the WorkerClient command lifecycle, the options validator, and the worker drain budget; race-sensitive | Not started |
| F | fix/c3-worker-residuals |
WRK-32, WRK-33 (+WRK-34 opportunistic) | sonnet | Small, precisely located worker edits + doc prose; cannot be built on macOS so the agent's job is a careful, minimal diff | Not started |
| G | fix/c3-session-lifecycle |
GWC-32, GWC-34, GWC-37 | opus | Distributor/dispose lifecycle races and an alarm-monitor re-resolve path; needs lock-discipline reasoning | Not started |
Out of scope for this execution (recorded, not dropped):
| ID | Status | Why |
|---|---|---|
| TST-32 | Blocked (external) | Gitea runner registration-token reset is a web-UI action on the Gitea admin page; no repo change. Operator to-do. |
| SEC-37 | Blocked (external) | Family-wide: TLS on the shared GLAuth lives in scadaproj/infra/glauth/ (separate repo + docker host). Plan separately; after it lands, move the deployed service to Production. |
| CLI-47/48/49/50, IPC-36, IPC-37, TST-33/34/35/36, GWC-33/35/36, SEC-38/39, IPC-38, WRK-34 | Deferred (P2) | Roadmap tier 10; WRK-34 may be picked up by batch F if trivial. |
Batch specifications
A — CLI-46 client release-train bump (sonnet)
- Bump .NET/Go/Python/Rust clients
0.2.0 → 0.2.1, Java0.2.1 → 0.2.2in every version-bearing file (pyproject.toml+version.py,Cargo.tomlworkspace + anyversion =consumers,Directory.Build.props/csproj<Version>, Javabuild.gradle/gradle.properties, Go module is tag-driven — update anyVersionconst if one exists). Followdocs/ClientPackaging.mdfor the authoritative file list. clients/python/README.md+docs/ClientPackaging.md: note that published pypi 0.2.0 has a brokenconnect()(event-loop binding, fixed bya8f86b5) and is superseded by 0.2.1.- Add a PR-time check to
scripts/check-codegen.ps1(or a newscripts/check-client-versions.ps1wired where the other checks run): fail if anyclients/<lang>source differs from its last publish tag while the version constant still equals that tag's version. Document indocs/ClientPackaging.md. - Verify:
dotnet build clients/dotnet/...slnx;cargo check --workspace;python -m pytest(clients/python);JAVA_HOME=/opt/homebrew/opt/openjdk@17 gradle testthen revert spurious generated-proto churn;go build ./... && go test ./....
B — TST-31 glauth.md + live-test credential (sonnet)
- Reconcile
glauth.mdagainst~/Desktop/scadaproj/infra/glauth/config.toml: ReadOnly gid, the pre-provisioned users table (dropreadonly/readonly123, fixadminpassword), the worked example at:124. Keep the OPC-UA taxonomy note. - Re-derive
MXGATEWAY_LIVE_MXACCESS_WRITE_SECURED_PASSWORDdefault indocs/GatewayTesting.mdand the?? "admin123"literal inWorkerLiveMxAccessSmokeTests.csfrom the config; make the test log (not assert) whetherAuthenticateUsersucceeded. - Verify:
dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx(IntegrationTests compiles on mac? if excluded, note it).
C — GWC-31 + tracker housekeeping (sonnet)
- Sweep code comments citing unregistered
GWC-30/GWC-31(perf pass) inWorkerClient.cs,GatewayMetrics.cs,WorkerClientTests.cs, workerWorkerFrameReader/Writer.cstoPERF-nnkeyed to task numbers indocs/plans/2026-08-15-perf-review-remediation.md; add a PERF-ID table to that plan. Do not touch the genuine GWC-30 (frame-reader prefix) citations if any exist. archreview/REVIEW-PROCESS.md(or wherever the ID rule lives): state that GWC/WRK/IPC/SEC/CLI/TST prefixes are reserved for archreview registers.- Cycle-2 tracker (
archreview/2026-07-12/remediation/00-tracking.md+ domain docs): GWC-16 → Won't fix (rationale from cycle-3 10-gateway-core.md), IPC-08 → Done (7c1ea12, residual IPC-34), IPC-31 → Done (superseded by GWC-28), refresh TST-06/TST-17 rows, register WRK-31 (interim ID burned bye913dab) so it is never reused. - Verify:
dotnet build src/ZB.MOM.WW.MxGateway.NonWindows.slnx;dotnet test --filter FullyQualifiedName~WorkerClientTests.
D — SEC-40, SEC-41, GWC-38 (opus)
- SEC-41:
DashboardSessionAclre-resolves tags fromLdapGroupClaimTypeclaims viaDashboardGroupTagMapping.MapGroupsToTagsagainst current options (asHubTokenServicedoes) instead of trusting login-stamped tag claims; keep stamped claims only as fallback when no group claims exist (document). Test: principal with stale tag claim + group whose grant was removed → denied. - SEC-40: decide tighten:
AlarmsHubgets a dedicated policy requiring a real role claim (anonymous-loopback/Disabled bypass excluded) — and document indocs/GatewayDashboardDesign.mdACL section +docs/GatewayConfiguration.mdGroupToTag row that alarm metadata remains galaxy-wide for authenticated Viewers (central monitor, no per-session provenance). Test for the policy. - GWC-38:
GatewayBrowseScopeProviderthrowsRpcException(PermissionDenied)(and logs) whenCurrentis null and API-key auth is enabled; unchanged when auth mode is Disabled. Test both branches. - Verify: build Server;
dotnet test --filter "FullyQualifiedName~DashboardSessionAcl|FullyQualifiedName~AlarmsHub|FullyQualifiedName~BrowseScope"(adjust to real class names).
E — IPC-33, IPC-39, IPC-34, IPC-35 (opus)
- IPC-33 option (a):
GatewayOptionsValidatoradds the upper couplingWorker:MaxMessageBytes ≤ Protocol:MaxGrpcMessageBytes + 2×reserve; workerResolveDrainReplyByteBudgetbecomesMaxMessageBytes − 2×DrainReplyFrameHeadroomBytes(floorMaxMessageBytes/2); updatedocs/GatewayConfiguration.md,docs/WorkerFrameProtocol.md. Test:GatewayOptionsTestscase for the new bound. - IPC-39: pre-check compares against
MaxMessageBytes − MaxSequenceEncodingBytes (11); fix the comment direction. - IPC-34: caller-cancellation arms call
TrySendCancelForAbandonedCommand(renamed) gated onremoved, skipped when_stopCtsis the source; newWorkerClientErrorCode.CallerCancelledreplacing theGatewayShutdownmislabel, mapped toCancelledin the gRPC layer; updatedocs/Grpc.mdmapping line,gateway.md:913-919,docs/GatewayProcessDesign.md:596-602. Test: twin of the timeout test driven by the caller token. - IPC-35:
ReadHandshakeEnvelopeAsyncspecial-casesWorkerFault→WorkerClientException(WorkerFaulted, CreateWorkerFaultMessage(...)). Test: fake writesWorkerFault{MxaccessCreationFailed}instead ofWorkerReady, category text surfaces. - Verify: build Server;
dotnet test --filter "FullyQualifiedName~WorkerClientTests|FullyQualifiedName~GatewayOptions". Worker half → windev afterwards.
F — WRK-32, WRK-33 (sonnet)
- WRK-33:
ObserveFaultWhenever(ObserveCommandTaskAsync(commandTask));atWorkerPipeSession.cs:1235(or catch-all-and-log in the observer). - WRK-32: one
pumpStep()before capturing theCurrentVersionbaseline inMxAccessCommandExecutor.cs; rewrite the executor doc comment (:996-1003) to state the recency-based contract; add the same todocs/MxAccessWorkerInstanceDesign.mdwrite-completion section and the reply-field comment inmxaccess_gateway.proto(comment-only proto edit → regenerate all generated artifacts in the same commit, per the repo rule; if regen tooling is unavailable on macOS, skip the proto comment and say so). - WRK-34 if trivial:
CreateFault(StaHung, "Watchdog", …)with the correlation id moved into the message. - Cannot build on macOS: keep diff minimal, net48-safe (no init-only/positional records). Verified on windev afterwards.
G — GWC-32, GWC-34, GWC-37 (opus)
- GWC-32:
DisposeAsynctransitions_statetoClosedunder_syncRootbefore completing channels (orif (_disposed || IsTerminalState()) return;inStageWorkerEvent); update the XML doc to name all three completion paths. Test: dispose a Ready client mid-dispatch → noProtocolViolationfault/metric. - GWC-34:
_eventDistributorDisposedflag set under_syncRootwhen nulling;EnsureDistributorCreatedthrowsSessionManagerException(SessionNotReady)when set. Test: attach after dispose → throws, no silent hang. - GWC-37: on
ApplyProviderModeChangeAsync(toMode == Subtag)re-resolve the watch list and re-subscribe; additionally surface "zero targets resolved while mode can need them" on the provider status (Degraded+ reason). Updatedocs/alarm-subtag section. Test with the fake worker: GR discovery fails at start, succeeds at failover → watch list non-empty after failover. - Verify: build Server;
dotnet test --filter "FullyQualifiedName~WorkerClientTests|FullyQualifiedName~GatewaySession|FullyQualifiedName~AlarmMonitor|FullyQualifiedName~AlarmWatchList".
Merge order
C is merged last (its comment sweep touches the same files E and G edit). Order: A, B, F, D, G, E, C. After merge: full NonWindows build + one full gateway test run; then windev worker CI for E+F.