docs(archreview): 2026-10-21 remediation tracker, cycle-1 re-triage, fix 2026-08-19 header

Add archreview/2026-10-21 (overall summary + tracker with R21-* rows).
Re-triage the 68 cycle-1 Not-started rows in place: 21 Done, 13 Won't fix,
34 Open (promoted as R21-O1..O34). Correct the 2026-08-19 tracker header
so CLI-47..50, IPC-36/37, TST-35 read Done like their rows.
This commit is contained in:
Joseph Doherty
2026-10-22 08:28:44 -04:00
parent 7f0720f956
commit ac4edc6e0a
4 changed files with 312 additions and 70 deletions
@@ -27,7 +27,7 @@ This plan is executed as **seven parallel batches**, each by one subagent workin
|---|---|---|
| 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/37, TST-35 | Not started | Activated by the 2026-08-25 P2 continuation below. |
| CLI-47/48/49/50, IPC-36/37, TST-35 | Done | Activated by the 2026-08-25 P2 continuation below and closed there: CLI-47/48/49/50 in batch K (`5cdb3da`), IPC-36/37 in batch J (`d6c5a11`), TST-35 in batch M (`cc143b5`). |
| GWC-33/35, SEC-38/39 | Done | Closed by continuation batch I; regression counts and commits are recorded below. |
| GWC-36, IPC-38, TST-33/34/36 | Done | Closed by continuation batch H; verification and commit are recorded below. |
| WRK-34 | Done | Completed opportunistically in batch F (`3679e59`): watchdog faults use `CommandMethod=Watchdog` and carry the stuck correlation ID in the diagnostic message. Tracker correction recorded by the continuation audit. |
+27
View File
@@ -0,0 +1,27 @@
# MxAccessGateway — System Review (2026-10-21)
Summary of the fourth review cycle. Per-finding status is in [`remediation/00-tracking.md`](remediation/00-tracking.md); the per-domain finding text is not kept in this directory, so the tracker rows carry the description.
## Dimensions
Six reviewers each took one dimension of the system.
| Dimension | Reviewed | Outcome |
|---|---|---|
| Sessions | Session lifecycle, sweep, shutdown, lease, metrics accounting (`GatewaySession`, `SessionManager`) | One High: a kill or shutdown during worker startup orphaned the worker. Nine findings, all fixed in stream A. Replay-ring byte cap remains open (low) |
| Worker | STA runtime, alarm polling, `ReadBulk`, caches, process exit paths (`ZB.MOM.WW.MxGateway.Worker`) | Eight findings fixed on `fix/review-2026-10-21-e`, not yet merged; four deferred (one needs a parity probe, one is a security tradeoff) |
| gRPC | Interceptor coverage, audit, alarm feed, correlation ids, `/metrics` | Eleven findings fixed. `/metrics` authentication is opt-in |
| Dashboard | Hub fan-out, browse, snapshot feed, health status, design doc | Ten findings fixed; three low races left open |
| Clients | Five language clients: ack retry, buffers, dispose, typed diagnostics | All fixed. The Go `go vet` copylocks error in a test predates the review and remains |
| Docs vs code | Proto comments, `stillpending.md`, tracker headers, untested helpers | In progress in stream L |
## Cross-cutting observations
- The two-process lifecycle is the most failure-prone area: most of the nine session findings are races between open, close, kill and shutdown. Each fix added a regression test; the pattern to keep is that every exit path accounts for the session exactly once.
- Silent failure was the recurring theme in the worker (STA death, alarm-poll failure, missing unhandled-exception hook). Each now ends in a typed fault or a distinct exit code.
- Bounded fan-out was the recurring theme in the dashboard: every hub send now has a bound, a timeout or a drop policy.
- Three external items are outside this repository: shared GLAuth TLS, the Gitea runner token, and an OtOpcUa session leak.
## Cycle-1 backlog
The review also re-triaged 68 first-cycle findings that no later tracker had re-listed: 21 are done, 13 are closed as won't fix with a reason, and 34 remain open and are promoted as `R21-O1` to `R21-O34`. The highest-weight open items are the missing reply to a command that races worker shutdown, the worker pipe without `CurrentUserOnly`, the no-op startup probe with retry options that cannot retry, the redactor gap for `WriteSecuredBulk`/`WriteSecured2Bulk`, the .NET retry budget that a `DeadlineExceeded` consumes, and the Go client's missing typed auth errors and strict-TLS flag.
@@ -0,0 +1,214 @@
# MxAccessGateway — Remediation Tracking (2026-10-21 system review)
Source review: [`../00-overall.md`](../00-overall.md) (summary of the six review dimensions). Generated 2026-10-22 from the review outcomes and the integration branch `fix/review-2026-10-21`.
Tracks the findings of the 2026-10-21 system review and the cycle-1 findings that no later tracker had re-listed. The earlier trackers remain the record for their own IDs: [cycle 1](../../remediation/00-tracking.md), [cycle 2](../../2026-07-12/remediation/00-tracking.md), [cycle 3](../../2026-08-19/remediation/00-tracking.md).
## How to use this document
- IDs in this cycle use the prefix `R21-` so they cannot collide with the `GWC`/`WRK`/`IPC`/`SEC`/`CLI`/`TST` registers of earlier cycles. `R21-S` is sessions, `R21-W` is the worker client, `R21-G` is the gRPC surface, `R21-D` is the dashboard, `R21-K` is the Windows worker, `R21-C*` are the client libraries, `R21-L` is docs and test gaps, `R21-X` is external, and `R21-O` is a cycle-1 finding re-triaged as still open.
- Where a finding is the same as an earlier-cycle ID, the description names it (for example `R21-G11` is `SEC-14`).
- The review's own severity table is not in the repository. Only `R21-S1` carries a severity from the review (High); every other severity here is the triage estimate from the finding's impact and is open to correction.
- The **Status** column is the single source of truth. Update it in the same change that lands the fix, and cite the finding ID in commit messages.
**Status legend:** `Done` · `Done (pending merge of stream E)` · `In progress (stream L)` · `Deferred` · `Won't fix` · `Open` · `Blocked (external)`.
## Execution model
The review was remediated by ten parallel streams on branches `fix/review-2026-10-21-{a..j}`, merged into the integration branch `fix/review-2026-10-21`, plus stream K (this tracker triage) and stream L (docs and test gaps).
| Stream | Branch | Scope | Status |
|---|---|---|---|
| A | `fix/review-2026-10-21-a` | Sessions (`R21-S*`) | Done, merged (`7f0720f`) |
| B | `fix/review-2026-10-21-b` | WorkerClient (`R21-W1`) | Done, merged (`81d4cd8`) |
| C | `fix/review-2026-10-21-c` | gRPC surface (`R21-G*`) | Done, merged (`3f9448f`) |
| D | `fix/review-2026-10-21-d` | Dashboard (`R21-D*`) | Done, merged (`8212b0e`) |
| E | `fix/review-2026-10-21-e` | Worker (`R21-K*`) | Done (pending merge of stream E) — `7618caf`, follow-up `38fbbde` |
| F | `fix/review-2026-10-21-f` | .NET client | Done, merged (`3a1ee28`) |
| G | `fix/review-2026-10-21-g` | Java client | Done, merged (`c18566b`) |
| H | `fix/review-2026-10-21-h` | Go client | Done, merged (`7636573`) |
| I | `fix/review-2026-10-21-i` | Rust client | Done, merged (`b2a7fc5`) |
| J | `fix/review-2026-10-21-j` | Python client | Done, merged (`29d4e51`) |
| K | `fix/review-2026-10-21-k` | Tracker triage (this document) | Done |
| L | (stream L branch) | Proto comments, docs, test gaps | In progress (stream L) |
Worker changes (stream E) need the windev x86 build and `Worker.Tests` run after merge; stream E's own live facts passed on windev.
## Sessions (stream A)
| ID | Sev | Area | Description | Status | Commit / stream | Reason / note |
|---|---|---|---|---|---|---|
| R21-S1 | High | Sessions | A kill or shutdown during worker startup orphaned the worker | Done | A: `87f23d9`, `ec9f264` | Gated attach, discard on any exception, `OpenRecorded`; a late worker is never orphaned |
| R21-S2 | Medium | Sessions | Sweep-driven closes skipped `SessionClosed()`, so the open-sessions gauge drifted | Done | A: `09666cc` | |
| R21-S3 | Medium | Sessions | `OpenSession` was still accepted while the host was stopping | Done | A: `37ab629` | `GatewayStopping` returns `Unavailable`; `ShutdownAsync` re-snapshots until the registry is empty |
| R21-S4 | Low | Sessions | A shutdown timeout whose kill succeeded was reported as `CloseFailed` | Done | A: `2b165cd` | Now `Closed`, with `WorkerShutdownFailure` recorded |
| R21-S5 | Low | Sessions | A failed open left a metrics entry behind | Done | A: `87f23d9` | Open rollback goes through the removal path |
| R21-S6 | Medium | Sessions | Sessions that never stream pinned a worker for the whole lease | Done | A: `285ef64`, `ec9f264` | Config-gated `UnattachedSessionGraceSeconds` (default 0 = off); `ExtendLease` deleted; internal subscribers count as attached; idle clock starts at Ready |
| R21-S7 | Low | Sessions | A kill racing a close double-decremented the gauge | Done | A: `ec9f264` | Found in review of stream A |
| R21-S8 | Low | Sessions | A sweep that lost the race to a close was reported as a failure | Done | A: `ec9f264` | Found in review of stream A |
| R21-S9 | Medium | Sessions | After `ShutdownWorker` the session stayed `Ready` | Done | A: `cc1e895` | The session faults and the sweep reaps it. Found live |
| R21-S10 | Low | Sessions | The replay ring has a count cap but no byte cap | Open | — | Documented as bounded by count. Next step: add a byte cap if large-array events make the count cap insufficient |
## WorkerClient (stream B)
| ID | Sev | Area | Description | Status | Commit / stream | Reason / note |
|---|---|---|---|---|---|---|
| R21-W1 | Medium | WorkerClient | `ShutdownAsync` could block past `ShutdownTimeout` | Done | B: `b0100e7` | The timeout CTS now covers the envelope enqueue |
## gRPC surface (stream C)
| ID | Sev | Area | Description | Status | Commit / stream | Reason / note |
|---|---|---|---|---|---|---|
| R21-G1 | Medium | gRPC | Denied bulk commands flooded the audit channel | Done | C: `ca0cf19` | One aggregated event per bulk call |
| R21-G2 | Medium | gRPC | The authorization interceptor did not cover client-streaming or duplex calls (cycle-1 `SEC-17`) | Done | C: `19cc74f` | Both fail closed to the admin scope |
| R21-G3 | Low | gRPC | `StreamAlarms` overflow returned `Unavailable` | Done | C: `0b4a168` | Now `ResourceExhausted` |
| R21-G4 | Low | gRPC | `StreamAlarms` cloned the snapshot under the lock on every attach | Done | C: `0b4a168` | Memoized projection |
| R21-G5 | Low | gRPC | `WriteAsync` was not given the call's cancellation token | Done | C: `0b4a168` | |
| R21-G6 | Low | gRPC | `correlation_id` handling was inconsistent across replies | Done | C: `0b4a168`, `82a856b` | `client_correlation_id` is echoed; a Debug log records the client/envelope pair |
| R21-G7 | Low | gRPC | The `StreamDisconnected` reason was a constant (cycle-1 `GWC-22`) | Done | C: `6ae5ed9` | |
| R21-G8 | Low | Security | `CachingApiKeyVerifier` index pruning and single-flight misses | Done | C: `ed3a2c9` | |
| R21-G9 | Low | gRPC | `ConstraintEnforcer` allocated a `Task` per item | Done | C: `ca0cf19` | `ValueTask` |
| R21-G10 | Low | Metrics | `Enum.ToString` was called per event | Done | C: `68d6b5f` | `MxEventFamilyNames` cache |
| R21-G11 | Medium | Security | `/metrics` was unauthenticated (cycle-1 `SEC-14`) | Done | C: `65bca80`, `82a856b` | Opt-in `Metrics:RequireAuthentication` (admin key, or dashboard Admin/Viewer cookie); `DisableLogin` does not bypass it; `/health` stays open by design |
| R21-G12 | Low | Metrics | The `/metrics` per-peer limiter is keyed by IP while the gRPC limiter is keyed by connection | Won't fix | — | Documented difference; the two surfaces have different peer identities |
| R21-G13 | Low | Security | The verifier generation guard depends on key-id case | Open | — | Latent only. Next step: normalize the key id (ordinal-ignore-case) at the guard and add a test |
## Dashboard (stream D)
| ID | Sev | Area | Description | Status | Commit / stream | Reason / note |
|---|---|---|---|---|---|---|
| R21-D1 | Medium | Dashboard | Unbounded events-hub sends | Done | D: `f1db002`, `bd7abf4` | One drop-oldest pump per session; a lock re-check race was fixed in review |
| R21-D2 | Medium | Dashboard | One slow client stalled the alarms hub | Done | D: `3688fa1` | 2 s send timeout plus an `AlarmFeedReset` marker. Tradeoff documented: an abandoned send is not followed by a reset |
| R21-D3 | Low | Dashboard | Browse search ran on every keystroke | Done | D: `db78336` | Debounce, off-dispatcher, indexed |
| R21-D4 | Medium | Dashboard | The full snapshot plus Configuration went out every second | Done | D: `6733519` | Configuration is sent once per connection; sends have a timeout |
| R21-D5 | Low | Dashboard | `AlarmsPage` re-sorted twice per render | Done | D: `2a7d6fa` | `DashboardAlarmView` memo |
| R21-D6 | Low | Dashboard | `BrowsePage` disposal was unbounded | Done | D: `db78336` | |
| R21-D7 | Low | Dashboard | Two viewers thrashed the advise LRU | Done | D: `5d4aefc` | Per-viewer share |
| R21-D8 | Low | Dashboard | Disconnected circuits kept polling | Done | D: `db78336` | 30 s retention plus a connection tracker |
| R21-D9 | Low | Dashboard | `GatewayStatus` was hardcoded (cycle-1 `SEC-27`) | Done | D: `6733519` | Derived from health checks |
| R21-D10 | Low | Docs | `docs/GatewayDashboardDesign.md` described counters that do not exist | Done | D: `2c963b7` | Doc rewritten to match the shipped pages |
| R21-D11 | Low | Dashboard | `BrowsePage` search CTS cancel/dispose race | Open | — | Only a caught `ObjectDisposedException`. Next step: swap-and-dispose under a lock when the page is next touched |
| R21-D12 | Low | Dashboard | Cached health status is stale while the publisher is idle | Open | — | Next step: refresh on first subscribe |
| R21-D13 | Low | Dashboard | Hub pumps are not awaited on broadcaster dispose | Open | — | Next step: await the pumps with a bounded timeout in `DisposeAsync` |
## Worker (stream E)
All rows are on `fix/review-2026-10-21-e` (`7618caf`, follow-up `38fbbde`), not yet merged. After merge, run `windev-worker-ci.ps1 -Mode test` and update these rows to plain `Done`.
| ID | Sev | Area | Description | Status | Commit / stream | Reason / note |
|---|---|---|---|---|---|---|
| R21-K1 | Medium | Worker | The write-completion cache and value cache were never evicted | Done (pending merge of stream E) | E: `7618caf`, `38fbbde` | Ghost-entry eviction on `AddItem` added in the follow-up |
| R21-K2 | Medium | Worker | `ReadBulk` was serial (N x timeout) | Done (pending merge of stream E) | E: `7618caf`, `38fbbde` | Shared deadline; dedupe and periodic message pumping in the follow-up |
| R21-K3 | Medium | Worker | One alarm-poll failure faulted the session (cycle-1 `WRK-05`) | Done (pending merge of stream E) | E: `7618caf` | Consecutive-failure threshold of 3 |
| R21-K4 | Low | Worker | The primary alarm consumer leaked when the standby build failed | Done (pending merge of stream E) | E: `7618caf` | |
| R21-K5 | Low | Worker | `StaRuntime` drained the whole queue before pumping messages | Done (pending merge of stream E) | E: `7618caf` | |
| R21-K6 | Medium | Worker | STA thread death after start was silent (cycle-1 `WRK-02`) | Done (pending merge of stream E) | E: `7618caf` | `LoopFault` becomes a `StaHung` WorkerFault |
| R21-K7 | Low | Worker | No unhandled-exception hook (cycle-1 `WRK-09`) | Done (pending merge of stream E) | E: `7618caf` | Exit code 7 |
| R21-K8 | Low | Worker | `RETRIGGER` handling was undocumented | In progress (stream L) | stream L | Documented as reserved, never synthesized; the proto comment is stream L's |
| R21-K9 | Medium | Tests | Live control-command facts for the worker (cycle-1 `TST-05` residual) | Done (pending merge of stream E) | E: `069972a`, `4ea3672` | Facts added and passing on windev |
| R21-K10 | Low | Worker | Unregister-first shutdown cleanup | Deferred | — | Needs a parity probe against the installed MXAccess before it can change cleanup order |
| R21-K11 | Medium | Security | A per-operator `AuthenticateUser` cache | Deferred | — | Security tradeoff (caching credentials' outcome); decide with the security owner before building |
| R21-K12 | Low | Worker | Batch task-completion-source allocation rework | Deferred | — | Low value |
| R21-K13 | Low | Worker | The alarm poll builds a full-BSTR `XmlDocument` every 500 ms | Deferred | — | Steady GC cost, not a correctness issue; revisit if profiling flags it |
## Clients (streams F to J)
| ID | Sev | Area | Description | Status | Commit / stream | Reason / note |
|---|---|---|---|---|---|---|
| R21-CN1 | Medium | .NET client | Acknowledge retried on transient failure | Done | F: `6f0b408` | No ack retry |
| R21-CN2 | Medium | .NET client | Client always owned its channel | Done | F: `6f0b408` | Caller-owned channel constructors |
| R21-CN3 | Medium | .NET client | `DisposeAsync` could throw or block (cycle-1 `CLI-06`) | Done | F: `6f0b408` | Safe, best-effort dispose |
| R21-CN4 | Low | .NET client | No typed Ping/GetSessionState/GetWorkerInfo/DrainEvents | Done | F: `6f0b408` | |
| R21-CN5 | Medium | .NET client | A session could not be re-attached by id (cycle-1 `CLI-05`) | Done | F: `6f0b408` | `AttachSession` |
| R21-CJ1 | Medium | Java client | Event queue was fixed at 16 | Done | G: `48c9323` | Configurable, default 1024 |
| R21-CJ2 | Medium | Java client | Overflow dropped buffered events | Done | G: `48c9323` | Overflow keeps buffered events |
| R21-CJ3 | Low | Java client | Interrupt did not cancel the call | Done | G: `48c9323` | |
| R21-CJ4 | Low | Java client | No typed diagnostics | Done | G: `48c9323` | |
| R21-CG1 | Medium | Go client | Event buffer fixed at 16 | Done | H: `a3a7bd2` | `EventBufferSize` option, default 1024 |
| R21-CG2 | Medium | Go client | Deprecated `DialContext` + `WithBlock` (cycle-1 `CLI-10`) | Done | H: `a3a7bd2` | `grpc.NewClient` |
| R21-CG3 | Low | Go client | No typed diagnostics | Done | H: `a3a7bd2` | |
| R21-CG4 | Low | Go client | `go vet` copylocks error in `types_alias_test.go` | Open | — | Pre-existing, not fixed by stream H. Next step: pass the type by pointer in the test; check `go vet` runs in CI |
| R21-CR1 | Medium | Rust client | Blocking file read on an async path (CA file) | Done | I: `0345726` | Async read |
| R21-CR2 | Low | Rust client | No typed diagnostics | Done | I: `0345726` | |
| R21-CP1 | Low | Python client | No typed diagnostics | Done | J: `99347d3`, `d716dec` | The client already had no ack retry, no event buffer, and cancels on exit |
## Docs and test gaps (stream L)
| ID | Sev | Area | Description | Status | Commit / stream | Reason / note |
|---|---|---|---|---|---|---|
| R21-L1 | Low | Docs | `operator_full_name` is silently dropped; proto comment and docs do not say so | In progress (stream L) | stream L | |
| R21-L2 | Low | Contracts | `RETRIGGER` and the `WorkerFaultCategory` values `INVALID_ARGUMENTS` and `GATEWAY_AUTHENTICATION_FAILED` are reserved but not labeled so | In progress (stream L) | stream L | Proto comments |
| R21-L3 | Low | Docs | Stale notes in `stillpending.md` (sections 1.4, 3.4, 7.1) and the deferred-followups header | In progress (stream L) | stream L | |
| R21-L4 | Low | Docs | The 2026-08-19 tracker header says CLI-47..50, IPC-36/37, TST-35 are `Not started` while the rows say `Done` | Done | K | Header row corrected in this stream |
| R21-L5 | Low | Tests | No tests for `GatewayConfigPathRules` | In progress (stream L) | stream L | |
| R21-L6 | Low | Tests | No tests for `DashboardRedactor` | In progress (stream L) | stream L | |
| R21-L7 | Low | Tests | No tests for `EventSubscriberReplayAttachment` | In progress (stream L) | stream L | |
| R21-L8 | Low | Tests | No tests for the `MxStatusDetailText` table | In progress (stream L) | stream L | |
## External / blocked
| ID | Sev | Area | Description | Status | Commit / stream | Reason / note |
|---|---|---|---|---|---|---|
| R21-X1 | Medium | Security | TLS on the shared GLAuth (cycle-3 `SEC-37`) | Blocked (external) | — | Lives in `scadaproj/infra/glauth/` (separate repo and docker host); after it lands, move the deployed service to `Production` |
| R21-X2 | Low | CI | Gitea runner registration-token reset (cycle-3 `TST-32`) | Blocked (external) | — | Web-UI action on the Gitea admin page |
| R21-X3 | Medium | OtOpcUa | `GalaxyDriver.ShutdownAsync` leaks sessions | Blocked (external) | — | Defect is in the OtOpcUa repo; the gateway's lease expiry and detach grace eventually reap a leaked session |
## Cycle-1 rows re-triaged as still open (promoted)
These are cycle-1 findings that no later tracker re-listed. Each was checked against the integration branch, and the cycle-1 row now points here. The `Medium` rows carry correctness, security or operability weight (`R21-O1`, `R21-O8`, `R21-O9`, `R21-O16`, `R21-O21` to `R21-O23`, `R21-O30`, `R21-O31`); the `Low` rows are hygiene to batch with the next change that touches the file.
| ID | Cycle-1 ID | Sev | Area | Description | Status | Next step |
|---|---|---|---|---|---|---|
| R21-O1 | WRK-03 | Medium | Worker | `TryStartCommandTask` returns silently when `_acceptingCommands` is false, so a command racing shutdown gets no reply and no log (`Ipc/WorkerPipeSession.cs:1240`) | Open | Reply `WorkerUnavailable` (at minimum log `WorkerCommandResultDropped`) and add `CommandAfterShutdown_RepliesWorkerUnavailable`; verify on windev |
| R21-O2 | WRK-08 | Low | Worker | Residual events queued after `WorkerShutdown` are discarded, and the shutdown section of `docs/MxAccessWorkerInstanceDesign.md` does not say so | Open | Document the discard as v1 behavior (docs-only) |
| R21-O3 | WRK-10 | Low | Worker | `WorkerApplication.Run` catch blocks log the exception type but never the message | Open | Add `exception_message` through `WorkerLogRedactor` in the three catch blocks; add a `WorkerApplicationTests` assertion |
| R21-O4 | WRK-16 | Low | Worker | Seven near-identical `CreateEnvelope` overload pairs and eight `WorkerPipeClient` constructors | Open | Refactor when `WorkerPipeSession` is next touched; existing IPC tests are the guard |
| R21-O5 | WRK-17 | Low | Worker | Gateway death surfaces as `EndOfStream` and exits 6 (`ProtocolViolation`) instead of 5 | Open | Map `EndOfStream` to `PipeConnectionFailed`; add a `WorkerApplicationTests` case |
| R21-O6 | WRK-18 | Low | Worker | Event-queue overflow exits as generic `UnexpectedFailure` (1) | Open | Add a dedicated exit code (stream E already uses 7 for unhandled exceptions, so use 8) and update the overflow paragraph of `docs/MxAccessWorkerInstanceDesign.md` |
| R21-O7 | WRK-19 | Low | Worker | The documented per-command start/end log with correlation id and latency does not exist | Open | Add level-gated `WorkerCommandStarted`/`WorkerCommandCompleted` in `StaCommandDispatcher.ExecuteQueuedCommandAsync` |
| R21-O8 | GWC-05 | Medium | Gateway | Worker pipe is created with no ACL and no `CurrentUserOnly` (`SessionWorkerClientFactory.CreatePipe`); only the unguessable name and `maxNumberOfServerInstances: 1` limit access | Open | Add `PipeOptions.CurrentUserOnly` (or a `PipeSecurity` for the worker account); confirm the worker runs as the same user on windev; add a launch test |
| R21-O9 | GWC-09 | Medium | Gateway | `WorkerProcessStartedProbe` only checks `HasExited`, so `StartupProbeRetryAttempts` and `StartupProbeRetryDelayMilliseconds` can never retry anything | Open | Either make the probe wait for a real readiness signal, or remove the two options and their docs so config does not promise a retry that cannot happen |
| R21-O10 | GWC-13 | Low | Gateway | Worker-ready wait is a 25 ms poll loop (`GatewaySession`, `pollIntervalMs = 25`) | Open | Replace with a `TaskCompletionSource` signalled on state change |
| R21-O11 | GWC-17 | Low | Gateway | `SparseArrayExpander` throws `Grpc.Core.RpcException` from the Sessions layer | Open | Throw a domain exception and map it to `InvalidArgument` in the gRPC layer |
| R21-O12 | GWC-18 | Low | Gateway | `GatewaySession` exposes `DisposeAsync` without implementing `IAsyncDisposable` | Open | Declare the interface; check `await using` call sites |
| R21-O13 | GWC-19 | Low | Gateway | `GatewaySession.KillWorker(string)` has no production callers (only test comments mention it) | Open | Delete it and fix the comments, or document it as the synchronous kill primitive |
| R21-O14 | GWC-20 | Low | Gateway | `HeartbeatCheckInterval` is fed from `HeartbeatIntervalSeconds`, conflating the send and check intervals | Open | Split the option or rename it, and update `docs/GatewayConfiguration.md` |
| R21-O15 | GWC-21 | Low | Gateway | `EventChannelFullModeTimeout` and `HeartbeatStuckCeiling` are compile-time constants (`WorkerClientOptions`) | Open | Expose both under `MxGateway:Worker:*` with validation, or record them as intentionally fixed |
| R21-O16 | SEC-13 | Medium | Security | `GatewayLogRedactor.SensitiveCommandMethods` lacks `WriteSecuredBulk` and `WriteSecured2Bulk`. Latent in cycle 1, but `GatewayLogRedactorSeam` now calls `RedactCommandValue`, so a secured-bulk payload would be logged unredacted if value logging is enabled | Open | Add both bulk kinds plus a redactor test; cross-check every command kind that carries a password |
| R21-O17 | SEC-16 | Low | Security | `apikey --pepper` is accepted on the command line (`ApiKeyAdminCommandLineParser`) and lands in shell history and the process list | Open | Prefer an env var or stdin prompt; keep the flag only with a warning |
| R21-O18 | SEC-18 | Low | Security | `DashboardApiKeyManagementService` detects a missing pepper by matching `"pepper unavailable"` in an exception message | Open | Catch a typed exception from `ZB.MOM.WW.Auth.ApiKeys`, or check the pepper before the call |
| R21-O19 | SEC-23 | Low | Security | The options validator does not reject a `__Host-` `CookieName` combined with `RequireHttpsCookie=false`, a cookie browsers silently drop | Open | Add the cross-field check to `ValidateDashboard` |
| R21-O20 | SEC-24 | Low | Security | `EffectiveDashboardConfiguration` omits `DisableLogin`, `AutoLoginUser`, `RequireHttpsCookie` and `CookieName`, so the Settings page cannot show that login is disabled | Open | Project the four fields in `GatewayConfigurationProvider` |
| R21-O21 | CLI-07 | Medium | .NET client | `ExecuteSafeUnaryAsync` wraps the retry pipeline in `CancelAfter(DefaultCallTimeout)` while each attempt has the same deadline, so a `DeadlineExceeded` first attempt consumes the whole budget and retries never run | Open | Make the outer budget `MaxAttempts x DefaultCallTimeout` (or drop the outer cap); add a wire test with a slow first attempt |
| R21-O22 | CLI-09 | Medium | Go client | Go wraps every RPC failure in `GatewayError`; there is no typed `Unauthenticated` vs `PermissionDenied` mapping, though `ClientLibrariesDesign.md` requires one | Open | Add sentinel or typed errors usable with `errors.Is`; keep credential redaction |
| R21-O23 | CLI-11 | Medium | Go client | `mxgw-go` has no flag for strict TLS validation, so `--tls` without a CA is always skip-verify | Open | Add `--require-certificate-validation` wired to `Options.RequireCertificateValidation` |
| R21-O24 | CLI-19 | Low | .NET client | `InternalsVisibleTo` is declared in both `Properties/AssemblyInfo.cs` and the csproj | Open | Delete one (cosmetic) |
| R21-O25 | CLI-22 | Low | Go client | `newCorrelationID` returns an empty string if `crypto/rand` fails | Open | Fall back to a counter-based id |
| R21-O26 | CLI-27 | Low | Python client | `Session.close()` has no lock around `_closed`, and a repeat close returns a synthesized reply | Open | Guard with an `asyncio.Lock` and cache the real reply, as .NET and Go do |
| R21-O27 | CLI-31 | Low | Rust client | The Rust CLI `main.rs` is now 3,023 lines | Open | Split by command group when next touched |
| R21-O28 | CLI-33 | Low | Clients | Slow-consumer behavior is documented in the Go README but not in the Java README or the cross-language `docs/ClientBehaviorFixtures.md` | Open | Add one comparison table now that this cycle changed the Go and Java buffers |
| R21-O29 | TST-07 | Low | Tests | Real-clock sleeps with negative assertions remain (for example `WorkerClientTests.cs:975`, 150 ms) | Open | Convert to a fake time provider where one exists; otherwise leave |
| R21-O30 | TST-09 | Medium | Ops | Health checks cover the auth store and sessions only; Galaxy SQL, LDAP and worker-executable presence are unchecked (`GatewayApplication.cs:118`) | Open | Add a Ready-tag worker-exe presence check first, then a bounded Galaxy SQL ping; zero sessions must stay Healthy because the Overview dashboard polls the Active tier |
| R21-O31 | TST-10 | Medium | Ops | No deploy or upgrade script or runbook for the NSSM-wrapped service | Open | Write a deployment doc from the windev/wonder deploy notes; respect the current ban on remote execution on the wonder boxes |
| R21-O32 | TST-18 | Low | Tests | `SessionLeaseMonitorHostedService` and `OrphanWorkerCleanupHostedService` timer/startup wiring has no direct tests | Open | Add timer and startup tests with a fake time provider |
| R21-O33 | TST-21 | Low | Ops | Serilog file sink rolls daily with no `retainedFileCountLimit` or size cap | Open | Set `retainedFileCountLimit` and `fileSizeLimitBytes` in `appsettings.json`; document |
| R21-O34 | TST-22 | Low | Docs | The Configuration Shape JSON block in `docs/GatewayConfiguration.md` omits `DisableLogin`, `AutoLoginUser`, `CookieName`, `Tls` and `Alarms:Fallback` | Open | Add the keys to the block (docs-only) |
## Cycle-1 triage summary
The 68 cycle-1 rows that were `Not started` with no later re-listing resolved as follows.
| Verdict | Count | Rows |
|---|:-:|---|
| Done | 21 | WRK-02, WRK-05, WRK-09, WRK-13, WRK-20, GWC-10, GWC-12, GWC-22, IPC-10, IPC-12, SEC-14, SEC-17, SEC-19, SEC-21, SEC-26, SEC-27, CLI-05, CLI-06, CLI-10, CLI-13, TST-14 |
| Won't fix | 13 | GWC-11, IPC-11, SEC-15, CLI-14, CLI-17, CLI-20, CLI-23, CLI-25, CLI-28, CLI-32, TST-19, TST-20, WRK-14 |
| Open (promoted) | 34 | `R21-O1` through `R21-O34` above |
`WRK-02`, `WRK-05` and `WRK-09` are closed by stream E and become plain `Done` when `fix/review-2026-10-21-e` merges.
The seven rows singled out for assessment: `WRK-03` Open (`R21-O1`: the worker still returns silently from `TryStartCommandTask` after shutdown starts); `WRK-05` Done (stream E); `CLI-06` Done (stream F); `CLI-07` Open (`R21-O21`: the outer `CancelAfter` still equals the per-attempt deadline); `CLI-09` Open (`R21-O22`: no typed auth errors in Go); `CLI-11` Open (`R21-O23`: no strict-TLS flag on `mxgw-go`); `CLI-13` Done (stream G).
## Change log
| Date | Change |
|---|---|
| 2026-10-22 | Initial tracker from the 2026-10-21 review outcomes. Stream E is recorded as done but unmerged; stream L items are in progress. 68 cycle-1 rows re-triaged and the 34 still-open ones promoted. |
+70 -69
View File
@@ -11,7 +11,7 @@ Source review: [`../00-overall.md`](../00-overall.md) · Per-domain remediation
- Do the work in **roadmap-tier order** (P0 → P1 → P2), respecting the `Dep` (depends-on) column within a tier.
- When a fix lands: flip Status to `Done`, and per the repo rule, update the affected docs in the same commit.
**Status legend:** `Not started` · `In progress` · `In review` · `Done` · `Won't fix` (record why in the domain doc) · `N/A` (informational / positive observation, no action).
**Status legend:** `Not started` · `In progress` · `In review` · `Done` · `Won't fix` (record why in the domain doc) · `N/A` (informational / positive observation, no action) · `Open` (re-triaged 2026-10-21: still present, promoted to the [2026-10-21 tracker](../2026-10-21/remediation/00-tracking.md)).
## Severity roll-up
@@ -63,24 +63,24 @@ Full design + implementation for each row lives in the linked domain doc under i
| GWC-02 | High | P0 | M | — | Done | Faulted sessions are never swept |
| GWC-03 | High | P0 | S | — | Done | Documented sparse-array max-length bound is unimplemented |
| GWC-04 | Medium | P1 | M | — | Done | Full event channel stalls the worker read loop behind command replies |
| GWC-05 | Medium | — | S | — | Not started | Worker pipe created with no ACL / no CurrentUserOnly |
| GWC-05 | Medium | — | S | — | Open | Worker pipe created with no ACL / no CurrentUserOnly — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O8 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| GWC-06 | Medium | P2 | S | GWC-07,08 | Done | Stopwatch allocated per streamed event |
| GWC-07 | Medium | P2 | S | GWC-06,08 | Done | Every mapped event is deep-cloned |
| GWC-08 | Medium | P2 | M | GWC-06,07 | Done | Pipe framing allocates per frame and writes twice |
| GWC-09 | Medium | — | M | — | Not started | Startup probe is a no-op; its retry pipeline can never retry |
| GWC-10 | Medium | — | S | — | Not started | Envelope `sequence` monotonicity specified but not enforced |
| GWC-11 | Low | — | S | — | Not started | `_workerClient` written under lock but read lock-free |
| GWC-12 | Low | — | S | — | Not started | `WorkerClient.DisposeAsync` not safe against double-dispose |
| GWC-13 | Low | — | M | — | Not started | Worker-ready wait is a 25 ms poll loop |
| GWC-09 | Medium | — | M | — | Open | Startup probe is a no-op; its retry pipeline can never retry — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O9 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| GWC-10 | Medium | — | S | — | Done | Envelope `sequence` monotonicity specified but not enforced — **2026-10-21 triage:** resolved by spec change, not enforcement: `gateway.md:440` states `sequence` is a diagnostic aid, stamped in each side's single write path and not validated on receive |
| GWC-11 | Low | — | S | — | Won't fix | `_workerClient` written under lock but read lock-free — **2026-10-21 triage:** reference reads are atomic and every ready/state decision reads `_workerClient` under `_syncRoot` (`EvaluateReadyUnderLock`); only diagnostic properties read it lock-free |
| GWC-12 | Low | — | S | — | Done | `WorkerClient.DisposeAsync` not safe against double-dispose — **2026-10-21 triage:** closed by cycle-3 GWC-32 (`DisposeAsync` claims `_disposed` under `_syncRoot`; see the 2026-08-19 tracker) |
| GWC-13 | Low | — | M | — | Open | Worker-ready wait is a 25 ms poll loop — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O10 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| GWC-14 | Low | P2 | M | — | Done | Replay ring is a `LinkedList` with a node alloc per event |
| GWC-15 | Low | P2 | S | — | Done | Per-event gauge reads `ChannelReader.Count` every event |
| GWC-16 | Low | — | S | — | Won't fix | `Invoke` resolves the session twice per command — measured negligible, deliberately not collapsed (see domain doc) |
| GWC-17 | Low | — | M | — | Not started | Sessions layer throws `Grpc.Core.RpcException` (layering leak) |
| GWC-18 | Low | — | S | — | Not started | `GatewaySession` implements `DisposeAsync` without `IAsyncDisposable` |
| GWC-19 | Low | — | S | — | Not started | Dead `GatewaySession.KillWorker(string)` + stale doc |
| GWC-20 | Low | — | S | — | Not started | Heartbeat config semantics conflated (send vs check interval) |
| GWC-21 | Low | — | S | GWC-04 | Not started | `EventChannelFullModeTimeout` / `HeartbeatStuckCeiling` not configurable |
| GWC-22 | Low | — | S | — | Not started | `StreamDisconnected` always labeled `"Detached"` |
| GWC-17 | Low | — | M | — | Open | Sessions layer throws `Grpc.Core.RpcException` (layering leak) — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O11 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| GWC-18 | Low | — | S | — | Open | `GatewaySession` implements `DisposeAsync` without `IAsyncDisposable` — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O12 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| GWC-19 | Low | — | S | — | Open | Dead `GatewaySession.KillWorker(string)` + stale doc — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O13 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| GWC-20 | Low | — | S | — | Open | Heartbeat config semantics conflated (send vs check interval) — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O14 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| GWC-21 | Low | — | S | GWC-04 | Open | `EventChannelFullModeTimeout` / `HeartbeatStuckCeiling` not configurable — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O15 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| GWC-22 | Low | — | S | — | Done | `StreamDisconnected` always labeled `"Detached"` — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-G7), `6ae5ed9`: `StreamDisconnected` records the real reason |
| GWC-23 | Info | — | — | — | N/A | `MaxEventSubscribersPerSession` dead in default single-subscriber mode |
### Worker — [20-worker.md](20-worker.md)
@@ -88,25 +88,25 @@ Full design + implementation for each row lives in the linked domain doc under i
| ID | Sev | Tier | Eff | Dep | Status | Title |
|---|---|:-:|:-:|---|---|---|
| WRK-01 | High | P0 | M | — | Done | Long `ReadBulk` self-faults as `StaHung`; all replies then dropped |
| WRK-02 | Medium | — | M | — | Not started | STA thread death after startup is silent; future work hangs forever |
| WRK-03 | Medium | — | S | WRK-02 | Not started | Commands after shutdown starts are dropped with no reply |
| WRK-02 | Medium | — | M | — | Done | STA thread death after startup is silent; future work hangs forever — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-K6): an STA loop fault raises a `StaHung` WorkerFault; stream E `7618caf`, pending merge of stream E |
| WRK-03 | Medium | — | S | WRK-02 | Open | Commands after shutdown starts are dropped with no reply — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O1 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| WRK-04 | Medium | — | M | — | Done | Envelope `sequence` can appear out of order on the wire |
| WRK-05 | Medium | — | M | — | Not started | One transient alarm-poll failure kills the whole session |
| WRK-05 | Medium | — | M | — | Done | One transient alarm-poll failure kills the whole session — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-K3): consecutive alarm-poll failure threshold of 3 in `MxAccessStaSession`; stream E `7618caf`, pending merge of stream E |
| WRK-06 | Medium | P2 | S | — | Done | `MXSTATUS_PROXY` conversion reflects per field, per event |
| WRK-07 | Medium | P1 | M | WRK-04 | Done | Documented outbound write priority not implemented |
| WRK-08 | Low | — | S | — | Not started | Residual event queue discarded at graceful shutdown, undocumented |
| WRK-09 | Low | — | S | — | Not started | No `AppDomain.UnhandledException` hook |
| WRK-10 | Low | — | S | — | Not started | Top-level catch logs exception type but never message |
| WRK-08 | Low | — | S | — | Open | Residual event queue discarded at graceful shutdown, undocumented — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O2 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| WRK-09 | Low | — | S | — | Done | No `AppDomain.UnhandledException` hook — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-K7): `AppDomain.UnhandledException` hook, exit code 7; stream E `7618caf`, pending merge of stream E |
| WRK-10 | Low | — | S | — | Open | Top-level catch logs exception type but never message — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O3 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| WRK-11 | Low | P2 | S | — | Done | Every accepted event is defensively cloned on enqueue |
| WRK-12 | Low | P2 | M | WRK-04 | Done | No event batching per envelope; one flush per event; 25 ms poll |
| WRK-13 | Low | — | S | — | Not started | Frame writer allocates a fresh buffer per frame; reader pools |
| WRK-14 | Low | — | M | — | Not started | Private-field naming split `_camelCase` vs `camelCase` |
| WRK-13 | Low | — | S | — | Done | Frame writer allocates a fresh buffer per frame; reader pools — **2026-10-21 triage:** worker `WorkerFrameWriter` rents the frame from `ArrayPool` (`Ipc/WorkerFrameWriter.cs:675`); the reader was already pooled |
| WRK-14 | Low | — | M | — | Won't fix | Private-field naming split `_camelCase` vs `camelCase` — **2026-10-21 triage:** cosmetic; the style guide permits both `_camelCase` and bare `camelCase` where established, and a mass rename is churn without behavior change |
| WRK-15 | Low | P2 | S | — | Done | Doc drift: STA thread name and heartbeat-counter note |
| WRK-16 | Low | — | S | — | Not started | Boilerplate duplication in IPC envelope/ctor overloads |
| WRK-17 | Low | — | S | — | Not started | Gateway death exits with wrong exit code (6 not 5) |
| WRK-18 | Low | — | S | — | Not started | Event-queue overflow exits as generic `UnexpectedFailure` |
| WRK-19 | Low | — | S | — | Not started | Command start/end logging with correlation id absent |
| WRK-20 | Low | — | M | WRK-01..04 | Not started | Test-coverage gaps for the failure modes above |
| WRK-16 | Low | — | S | — | Open | Boilerplate duplication in IPC envelope/ctor overloads — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O4 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| WRK-17 | Low | — | S | — | Open | Gateway death exits with wrong exit code (6 not 5) — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O5 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| WRK-18 | Low | — | S | — | Open | Event-queue overflow exits as generic `UnexpectedFailure` — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O6 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| WRK-19 | Low | — | S | — | Open | Command start/end logging with correlation id absent — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O7 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| WRK-20 | Low | — | M | WRK-01..04 | Done | Test-coverage gaps for the failure modes above — **2026-10-21 triage:** STA-death, sequence-monotonic, long-ReadBulk and late-reply tests exist; the one residual (command-after-shutdown reply) is carried by WRK-03 as R21-O1 |
### Contracts & IPC — [30-contracts-ipc.md](30-contracts-ipc.md)
@@ -121,9 +121,9 @@ Full design + implementation for each row lives in the linked domain doc under i
| IPC-07 | Medium | P2 | S | — | Done | `docs/Grpc.md` says six RPCs; there are seven |
| IPC-08 | Medium | — | M | — | Done | `WorkerCancel` defined and handled but never sent — resolved by `7c1ea12`/`a1a38b5`; residual (caller-cancellation leg) tracked as cycle-3 IPC-34 |
| IPC-09 | Medium | P1 | M | IPC-01 | Done | Known codegen fragilities have no in-repo guards |
| IPC-10 | Low | — | S | — | Not started | Envelope `sequence` is write-only; monotonicity never validated |
| IPC-11 | Low | — | S | — | Not started | No protocol version negotiation despite `supported_protocol_version` name |
| IPC-12 | Low | — | S | — | Not started | Gateway frame writer has no write lock; integrity rests on undocumented invariant |
| IPC-10 | Low | — | S | — | Done | Envelope `sequence` is write-only; monotonicity never validated — **2026-10-21 triage:** same resolution as GWC-10: `gateway.md:440` documents `sequence` as a diagnostic aid |
| IPC-11 | Low | — | S | — | Won't fix | No protocol version negotiation despite `supported_protocol_version` name — **2026-10-21 triage:** the review itself recommended no change; exact-match protocol version is correct for a lockstep-deployed pair, and the next bump should introduce a min/max range |
| IPC-12 | Low | — | S | — | Done | Gateway frame writer has no write lock; integrity rests on undocumented invariant — **2026-10-21 triage:** IPC-13 collapsed the gateway write to one pooled-buffer write, and `WorkerClient.WriteLoopAsync` is its only caller |
| IPC-13 | Low | P2 | S | IPC-05 | Done | Gateway writer serializes twice and issues two stream writes |
| IPC-14 | Low | P2 | S | IPC-05 | Done | Gateway reader allocates fresh array per frame; worker rents from `ArrayPool` |
| IPC-15 | Low | P2 | M | — | Done | Worker event delivery 25 ms poll with per-event write+flush, no batching |
@@ -151,21 +151,21 @@ Full design + implementation for each row lives in the linked domain doc under i
| SEC-10 | Medium | P1 | L | — | Done | No API-key expiry |
| SEC-11 | Medium | P1 | M | SEC-08 | Done | No rate limiting or lockout on either auth surface |
| SEC-12 | Medium | P1 | M | — | Done | Dashboard Close/Kill bypass the canonical audit store |
| SEC-13 | Low | — | S | SEC-30 | Not started | Redactor's credential-command list omits secured-bulk variants |
| SEC-14 | Low | — | S | SEC-02,20 | Not started | `/metrics` + `/health` unauthenticated; a metric leaks session ids |
| SEC-15 | Low | — | S | — | Not started | GET `/logout` skips antiforgery |
| SEC-16 | Low | — | S | — | Not started | CLI accepts the pepper as a command-line argument |
| SEC-17 | Low | — | S | — | Not started | Interceptor does not override client-streaming/duplex handlers |
| SEC-18 | Low | — | S | — | Not started | Pepper-unavailable detection matches library message text |
| SEC-19 | Low | — | S | — | Not started | Canonical audit store re-issues `CREATE TABLE` per write/read |
| SEC-13 | Low | — | S | SEC-30 | Open | Redactor's credential-command list omits secured-bulk variants — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O16 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| SEC-14 | Low | — | S | SEC-02,20 | Done | `/metrics` + `/health` unauthenticated; a metric leaks session ids — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-G11), `65bca80` + `82a856b`: opt-in `Metrics:RequireAuthentication`; `/health` stays open by design |
| SEC-15 | Low | — | S | — | Won't fix | GET `/logout` skips antiforgery — **2026-10-21 triage:** accepted nuisance: GET `/logout` skips antiforgery deliberately (comment in `DashboardEndpointRouteBuilderExtensions.cs`); the POST path validates antiforgery |
| SEC-16 | Low | — | S | — | Open | CLI accepts the pepper as a command-line argument — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O17 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| SEC-17 | Low | — | S | — | Done | Interceptor does not override client-streaming/duplex handlers — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-G2), `19cc74f`: client- and duplex-streaming calls authenticate and fail closed to admin |
| SEC-18 | Low | — | S | — | Open | Pepper-unavailable detection matches library message text — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O18 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| SEC-19 | Low | — | S | — | Done | Canonical audit store re-issues `CREATE TABLE` per write/read — **2026-10-21 triage:** `SqliteCanonicalAuditStore` latches schema creation, so `CREATE TABLE IF NOT EXISTS` runs once (a failure leaves the latch open) |
| SEC-20 | Low | P1 | S | — | Done | `session_id` metric tag is unbounded cardinality |
| SEC-21 | Low | — | M | — | Not started | Snapshot publisher works every second regardless of audience |
| SEC-21 | Low | — | M | — | Done | Snapshot publisher works every second regardless of audience — **2026-10-21 triage:** `DashboardSnapshotFeed` is idle-gated (no viewers, no timer), API-key summaries refresh every 15 s, and configuration is sent once per connection (R21-D4, `6733519`) |
| SEC-22 | Low | P2 | M | — | Done | `docs/Authentication.md` documents types no longer in this repo |
| SEC-23 | Low | — | S | SEC-03,04 | Not started | Validator ignores `DisableLogin`/`AutoLoginUser`/`RequireHttpsCookie`/`CookieName` |
| SEC-24 | Low | — | S | SEC-04 | Not started | Effective-config view omits the riskiest dashboard flags |
| SEC-23 | Low | — | S | SEC-03,04 | Open | Validator ignores `DisableLogin`/`AutoLoginUser`/`RequireHttpsCookie`/`CookieName` — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O19 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| SEC-24 | Low | — | S | SEC-04 | Open | Effective-config view omits the riskiest dashboard flags — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O20 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| SEC-25 | Low | P2 | M | SEC-02 | Done | Per-session EventsHub ACL is an acknowledged TODO |
| SEC-26 | Low | — | M | — | Not started | Audit trail has no retention or pruning |
| SEC-27 | Low | — | S | — | Not started | Dashboard `GatewayStatus` is hardcoded Healthy |
| SEC-26 | Low | — | M | — | Done | Audit trail has no retention or pruning — **2026-10-21 triage:** `Security/Audit/AuditDrainService.cs` prunes by `AuditRetentionDays` (validated in `GatewayOptionsValidator`) on an hourly sweep |
| SEC-27 | Low | — | S | — | Done | Dashboard `GatewayStatus` is hardcoded Healthy — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-D9), `6733519`: `GatewayStatus` derives from health checks |
| SEC-28 | Info | — | S | — | N/A | Positive security observations (preserve under change) |
| SEC-29 | Info | — | S | — | N/A | UI-stack rule verified compliant |
| SEC-30 | Info | P2 | S | SEC-13 | Done | Value-logging feature is unwired end-to-end |
@@ -178,35 +178,35 @@ Full design + implementation for each row lives in the linked domain doc under i
| CLI-02 | High | P1 | M | — | Done | Rust crate unbuildable outside the repo (`build.rs` path + `--no-verify`) |
| CLI-03 | High | P0 | M | — | Done | Rust `invoke` never validates HRESULT / MXSTATUS_PROXY |
| CLI-04 | High | P2 | L | CLI-15 | Done | Typed-command parity gap across all five clients |
| CLI-05 | Medium | — | S | — | Not started | .NET session cannot be re-attached to an existing session id |
| CLI-06 | Medium | — | S | — | Not started | .NET `DisposeAsync` blocks/throws on unreachable gateway |
| CLI-07 | Medium | — | S | — | Not started | .NET retry budget self-defeats on `DeadlineExceeded` |
| CLI-05 | Medium | — | S | — | Done | .NET session cannot be re-attached to an existing session id — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-CN5), `6f0b408`: `MxGatewayClient.AttachSession` |
| CLI-06 | Medium | — | S | — | Done | .NET `DisposeAsync` blocks/throws on unreachable gateway — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-CN3), `6f0b408`: `MxGatewaySession.DisposeAsync` is best-effort and never throws |
| CLI-07 | Medium | — | S | — | Open | .NET retry budget self-defeats on `DeadlineExceeded` — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O21 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| CLI-08 | Medium | — | S | CLI-03 | Done | .NET/Go/Java treat any nonzero HRESULT as failure (should be `< 0`) — landed via 2026-07-12 [CLI-38](../2026-07-12/remediation/50-clients.md#cli-38--align-netgojava-on-hresult--0-lands-prior-cli-08-cures-the-doc-drift---medium--p1) |
| CLI-09 | Medium | — | M | — | Not started | Go has no typed auth-error mapping (Unauthenticated vs PermissionDenied) |
| CLI-10 | Medium | — | M | — | Not started | Go uses deprecated `grpc.DialContext` + `grpc.WithBlock()` |
| CLI-11 | Medium | — | S | — | Not started | Go CLI cannot opt into strict TLS validation |
| CLI-09 | Medium | — | M | — | Open | Go has no typed auth-error mapping (Unauthenticated vs PermissionDenied) — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O22 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| CLI-10 | Medium | — | M | — | Done | Go uses deprecated `grpc.DialContext` + `grpc.WithBlock()` — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-CG2), `a3a7bd2`: `grpc.NewClient` replaces `DialContext` + `WithBlock` |
| CLI-11 | Medium | — | S | — | Open | Go CLI cannot opt into strict TLS validation — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O23 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| CLI-12 | Medium | P2 | S | — | Done | Java docs still say "Java 21" after the JDK 17 retarget |
| CLI-13 | Medium | — | M | — | Not started | Java event-stream buffer hardcoded 16, cancel-on-overflow |
| CLI-14 | Medium | — | S | — | Not started | Python default TLS is blocking TOFU pin with silent `localhost` SNI |
| CLI-13 | Medium | — | M | — | Done | Java event-stream buffer hardcoded 16, cancel-on-overflow — **2026-10-21 triage:** closed by the 2026-10-21 review (R21-CJ1/CJ2), `48c9323`: event queue is configurable (default 1024) and overflow keeps buffered events |
| CLI-14 | Medium | — | S | — | Won't fix | Python default TLS is blocking TOFU pin with silent `localhost` SNI — **2026-10-21 triage:** documented and intentional: the Python README describes the trust-on-first-use default and `--require-certificate-validation`; the `localhost` SNI default matches the gateway's self-signed SAN (`options.py:160`) |
| CLI-15 | Medium | P2 | M | — | Done | No client handles `ReplayGap` or offers a reconnect helper |
| CLI-16 | Medium | P2 | S | CLI-12 | Done | `docs/ClientPackaging.md` drifted from Python naming and .NET `.slnx` |
| CLI-17 | Medium | — | M | CLI-14 | Not started | TLS default posture inconsistent across the five clients |
| CLI-17 | Medium | — | M | CLI-14 | Won't fix | TLS default posture inconsistent across the five clients — **2026-10-21 triage:** documented divergence (`docs/CrossLanguageSmokeMatrix.md`); a strict default would break every deployment on the gateway's self-signed certificate. Strict opt-in exists per client; the Go CLI gap is R21-O23 |
| CLI-18 | Low | P2 | S | — | Done | .NET csproj records no `<Version>` |
| CLI-19 | Low | — | S | — | Not started | .NET duplicate `InternalsVisibleTo` (AssemblyInfo + csproj) |
| CLI-20 | Low | — | S | CLI-17 | Not started | .NET `--tls` without CA installs accept-all callback |
| CLI-19 | Low | — | S | — | Open | .NET duplicate `InternalsVisibleTo` (AssemblyInfo + csproj) — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O24 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| CLI-20 | Low | — | S | CLI-17 | Won't fix | .NET `--tls` without CA installs accept-all callback — **2026-10-21 triage:** same decision as CLI-17: documented, intentional default; `RequireCertificateValidation` opts in |
| CLI-21 | Low | P2 | S | — | Done | Go `ClientVersion = "0.1.0-dev"` stale vs tagged releases |
| CLI-22 | Low | — | S | — | Not started | Go `newCorrelationID` swallows `crypto/rand` error → empty id |
| CLI-23 | Low | — | S | — | Not started | Go nil-vs-empty bulk short-circuit asymmetry |
| CLI-22 | Low | — | S | — | Open | Go `newCorrelationID` swallows `crypto/rand` error → empty id — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O25 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| CLI-23 | Low | — | S | — | Won't fix | Go nil-vs-empty bulk short-circuit asymmetry — **2026-10-21 triage:** cosmetic; an empty bulk is a local no-op in some helpers and a wire call in others |
| CLI-24 | Low | — | S | — | Done | Java `MxEventStream` single-consumer constraint undocumented (closed 2026-08-07 per 2026-07-12 review old-tracker action; documented at MxEventStream.java:25 "Single consumer") |
| CLI-25 | Low | — | S | — | Not started | Java `close()` does not await channel termination |
| CLI-25 | Low | — | S | — | Won't fix | Java `close()` does not await channel termination — **2026-10-21 triage:** `closeAndAwaitTermination()` is the documented awaiting path; the leak window is bounded by JVM exit |
| CLI-26 | Low | P2 | S | — | Done | Python `version.py` (0.1.0) ≠ `pyproject.toml` (0.1.2) |
| CLI-27 | Low | — | S | — | Not started | Python `Session.close()` not concurrency-safe; synthesizes reply |
| CLI-28 | Low | — | S | — | Not started | Python circular-import workaround at end of `session.py` |
| CLI-27 | Low | — | S | — | Open | Python `Session.close()` not concurrency-safe; synthesizes reply — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O26 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| CLI-28 | Low | — | S | — | Won't fix | Python circular-import workaround at end of `session.py` — **2026-10-21 triage:** the import cycle is deliberate, marked `# noqa: E402`, and covered by the Python test suite |
| CLI-29 | Low | P2 | S | — | Done | Rust `CLIENT_VERSION` (0.1.0-dev) ≠ `Cargo.toml` (0.1.2) |
| CLI-30 | Low | — | S | CLI-04 | Done | Rust has no `unregister` typed helper |
| CLI-31 | Low | — | M | — | Not started | Rust CLI is a single 2,699-line `main.rs` |
| CLI-32 | Low | — | S | — | Not started | Client-side bulk caps differ (.NET/Java unbounded) |
| CLI-33 | Low | — | S | CLI-01,13 | Not started | Per-language event backpressure semantics undocumented |
| CLI-31 | Low | — | M | — | Open | Rust CLI is a single 2,699-line `main.rs` — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O27 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| CLI-32 | Low | — | S | — | Won't fix | Client-side bulk caps differ (.NET/Java unbounded) — **2026-10-21 triage:** the gateway is the authority for bulk caps; the divergence affects only the error type for an oversized call |
| CLI-33 | Low | — | S | CLI-01,13 | Open | Per-language event backpressure semantics undocumented — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O28 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| CLI-34 | Low | — | S | — | Done | Python `build/`/`.pytest_cache/` present on disk (untracked) (closed 2026-08-07 per 2026-07-12 review old-tracker action; both gitignored in clients/python/.gitignore) |
### Testing, docs & gaps — [60-testing-docs-gaps.md](60-testing-docs-gaps.md)
@@ -219,22 +219,22 @@ Full design + implementation for each row lives in the linked domain doc under i
| TST-04 | High | P2 | L | — | Done | Session-resilience epic 16/28 tasks unfinished |
| TST-05 | Medium | P1 | S | TST-03 | Partially done | Real-worker control/COM paths verified opt-in only. **Scheduling half closed 2026-08-10** by the TST-25 `nightly-windev` job (`.gitea/workflows/ci.yml`, cron `0 6 * * *` → `scripts/ci/run-windev-ci.sh live`, which sets `MXGATEWAY_RUN_LIVE_MXACCESS_TESTS=1`, runs `WorkerLiveMxAccessSmokeTests`, and opens a Gitea issue on failure) — "opt-in, run by memory" is now "runs nightly, reports failures". **Residual: the coverage-audit half.** The live suite's 8 facts cover all six late-added COM commands but none of the five control commands (`Ping`, `GetSessionState`, `GetWorkerInfo`, `DrainEvents`, `ShutdownWorker`), which real workers answer in `Worker/Ipc/WorkerPipeSession.cs` yet are still only exercised through `FakeWorkerHarness` canned replies — precisely the masking the finding named |
| TST-06 | Medium | — | M | — | Materially narrowed | Dashboard live-data path untested — `DashboardLiveDataServiceTests`, `EventsHubTests`, `EventsHubViewerRegistryTests`, `AlarmsHubPublisherTests`, `SessionDetailsPageEventAclTests` now exist (landed with SEC-25/TST-16); needs a re-audit-and-close pass, not re-implementation |
| TST-07 | Medium | — | S | — | Not started | Real-clock sleeps with negative assertions are latent flakes |
| TST-07 | Medium | — | S | — | Open | Real-clock sleeps with negative assertions are latent flakes — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O29 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| TST-08 | Medium | P1 | M | — | Done | Full-suite orphaned testhost processes (does not reproduce; doc de-stale) |
| TST-09 | Medium | — | M | — | Not started | Health checks cover only the auth store |
| TST-10 | Medium | — | M | — | Not started | Deployment/upgrade undocumented and hand-rolled |
| TST-09 | Medium | — | M | — | Open | Health checks cover only the auth store — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O30 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| TST-10 | Medium | — | M | — | Open | Deployment/upgrade undocumented and hand-rolled — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O31 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| TST-11 | Medium | P2 | M | — | Done | No version discipline on server; client versions drift |
| TST-12 | Medium | P0 | S | — | Done | CLAUDE.md misstates default retention behaviour |
| TST-13 | Medium | P2 | S | — | Done | gateway.md carries stale design-era sketches |
| TST-14 | Medium | P2 | S | — | Not started | Repo-root working artifacts need triage |
| TST-14 | Medium | P2 | S | — | Done | Repo-root working artifacts need triage — **2026-10-21 triage:** root `oldtasks.md` and the `*-docs-*.md` working files are no longer in the tree; `stillpending.md` remains as the backlog and stream L is refreshing it |
| TST-15 | Medium | P2 | M | TST-04 | Done | Dashboard EventsHub has no per-session ACL |
| TST-16 | Medium | P1 | S | — | Done | `Dashboard:ShowTagValues` is a dead flag |
| TST-17 | Medium | — | S | — | Materially changed | Vendor-gated alarm parity residuals silently lossy — `WnWrapAlarmConsumer.cs:309-327` now passes `szDomainName`/`szOprFullName`; the vendor 8-arg leg is recorded "unavailable by configuration" with evidence (`docs/AlarmProbeFindings.md`, `d1ae43d`, `540a118`); "silent drop, unchanged" is no longer accurate — needs a worker-domain refresh pass |
| TST-18 | Low | — | S | — | Not started | Hosted-service wrappers untested |
| TST-19 | Low | — | S | — | Not started | Keep FakeWorkerHarness canned replies in lockstep |
| TST-20 | Low | — | S | — | Not started | E2E script papers over a real advise-without-consumer sharp edge |
| TST-21 | Low | — | S | — | Not started | Log rotation configured but minimal |
| TST-22 | Low | — | S | — | Not started | Config-shape JSON block omits documented keys |
| TST-18 | Low | — | S | — | Open | Hosted-service wrappers untested — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O32 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| TST-19 | Low | — | S | — | Won't fix | Keep FakeWorkerHarness canned replies in lockstep — **2026-10-21 triage:** ongoing discipline rather than a work item; the harness shares production framing and the nightly live tier (`nightly-windev`) catches canned-reply drift |
| TST-20 | Low | — | S | — | Won't fix | E2E script papers over a real advise-without-consumer sharp edge — **2026-10-21 triage:** documented sharp edge in `docs/GatewayTesting.md`; the e2e script's interleaved drains are the intended workaround |
| TST-21 | Low | — | S | — | Open | Log rotation configured but minimal — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O33 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| TST-22 | Low | — | S | — | Open | Config-shape JSON block omits documented keys — **2026-10-21 triage:** still present on the integration branch; promoted to R21-O34 in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) |
| TST-23 | Low | P2 | S | — | Done | Bidirectional `Session` RPC never built |
| TST-24 | Low | P2 | M | TST-03 | Done | Client wire behaviour has no automated verification — closed 2026-08-10. Go/Rust/Java already had real-server wire tests (the finding's premise was stale); the genuine gaps were .NET (transport-interface fake everywhere, no server package) and Python (stub monkeypatch everywhere but one opt-in TLS test). Added `MxGatewayClientWireTests` + `WireFakeGatewayServer` (Kestrel h2c) and `tests/test_wire_fake_gateway.py` (`grpc.aio` loopback), plus a `dotnet test` step for the .NET client in the `portable` CI job. Caught a real bug: Python `connect()` built the `grpc.aio` channel inside `asyncio.to_thread` and failed for every non-stub connection |
@@ -253,6 +253,7 @@ Findings the review flagged as one coordinated design pass — sequence them tog
| Date | Change |
|---|---|
| 2026-10-22 | **Cycle-1 `Not started` rows re-triaged against the 2026-10-21 integration branch** (stream K). All 68 rows that no later tracker had re-listed now carry a verdict: 21 `Done` (closed by earlier work or by the 2026-10-21 review), 13 `Won't fix` (reason on the row), 34 `Open` (promoted as `R21-O1`..`R21-O34` in [`../2026-10-21/remediation/00-tracking.md`](../2026-10-21/remediation/00-tracking.md) with a next step). Rows closed by stream E are marked pending merge of that stream. |
| 2026-08-17 | **TST-16 → `Done`: `Dashboard:ShowTagValues` now gates every dashboard value surface** (branch `feat/followup-closeout`). The flag was live on one seam only (the events-hub mirror, shipped with SEC-25); the two recorded residuals are closed, so it is no longer a partly-dead flag. **Alarms hub:** `AlarmsHubPublisher` took `IOptions<GatewayOptions>` and now clears `current_value`/`limit_value` from a **deep clone** of each `AlarmFeedMessage` on both value-bearing payload arms (`transition`, `active_alarm`) before the `/hubs/alarms` broadcast. Clone-only, matching `DashboardEventBroadcaster.RedactValues`: the source message fans out from one feed to gRPC `StreamAlarms` subscribers and the alarms page, none of which this dashboard-display flag governs. The redaction switches on the value-bearing arms and passes everything else through untouched and uncloned, so a payload arm added later (the `snapshot_status` arm landing alongside this change) is forwarded unbroken rather than silently dropped. **`/browse`:** the gate lives at the service boundary per TST-16's own recommendation — `DashboardLiveDataService` substitutes `DashboardTagValue.RedactedValueText` (`[redacted]`) for the formatted value text, so the value never enters the render tree and `BrowsePage.razor` needed no change; data type, quality, source timestamp, and read errors stay real, keeping the panel diagnostic. A visible marker, not a blank, so a suppressed value is distinguishable from a tag that read back empty. Tests: new `AlarmsHubPublisherTests` (4 — both arms redacted with metadata intact, source not mutated, `true` passes values through as the same instance, valueless arms forwarded as the same instance) and two `DashboardLiveDataServiceTests` cases (`false` → `[redacted]`, `true` → formatted); `DashboardEventBroadcasterTests` re-run as regression. Docs: `docs/GatewayConfiguration.md`'s `ShowTagValues` row rewritten from "gates only the events hub mirror, two still-open residuals" to the three seams it now covers, with the "no gRPC client is affected" scope statement; `docs/GatewayDashboardDesign.md` gained the redaction note on the `AlarmsHub` row and had its Browse-panel paragraph corrected (it claimed the panel "always shows values regardless of `Dashboard:ShowTagValues`"). |
| 2026-08-17 | **Alarm-snapshot truncation now has a structural degraded-status signal** (branch `feat/deferred-closeout`, commits `693a78d` + `b9fb0dd`). No review ID — this is branch work outside the 153-finding register, recorded here so the tracker is not silent on a shipped change to the alarm surface. Before it, a capped `GetXmlCurrentAlarms2` fetch suppressed absence-implies-Clear inference and said so only in a rate-limited worker stderr warning, so no client and no operator could tell a complete active set from a capped one. Two additive proto3 booleans carry the verdict out — `QueryActiveAlarmsReplyPayload.snapshot_truncated = 2` and `ActiveAlarmSnapshot.from_truncated_snapshot = 16` (per record, because `QueryActiveAlarms` is a bare `stream ActiveAlarmSnapshot` with no envelope; the reply payload states it too, since a prefix filter can leave zero records and a capped fetch with nothing to report still has to say so). Flow: `WnWrapAlarmConsumer` → `AlarmDispatcher` / `IAlarmCommandHandler` → `MxAccessCommandExecutor` reply → `GatewayAlarmMonitor` → `IGatewayAlarmService.SnapshotTruncated` → `AlarmsPage` banner. `b9fb0dd` then made the pairing structural after review: `IMxAccessAlarmConsumer` and `IAlarmCommandHandler` expose one accessor (`SnapshotActiveAlarms(out bool truncated)` / `QueryActive(..., out bool snapshotTruncated)`) satisfied from a single lock acquisition, so the snapshot and its verdict can no longer be read across a poll; the separate `LastSnapshotTruncated` property is gone from every layer. Detection is deliberately unchanged (`fetchedRecordCount >= maxAlarmsPerFetch`); switching to `ALARM_RECORDS/@COUNT` stays blocked on probe evidence (`docs/AlarmProbeFindings.md`). Not latched, and dropped with the cache generation by `ClearCache`. Additive gateway metadata about our fetch mechanics, not MXAccess behaviour — no synthesized event, so not a parity deviation. Docs: `gateway.md` alarm surface, `docs/DesignDecisions.md`. |
| 2026-08-17 | **TST-15 → `Done` (discharges the ACL half of SEC-25): per-session dashboard event ACL shipped** (branch `feat/deferred-closeout`, commits `693a78d` + `7ec0b35`). Implements `docs/plans/2026-07-10-dashboard-session-acl-tst15.md`, whose header is now `Implemented` with as-built notes in its §12. `IDashboardSessionAcl.CanViewSession` is the single decision **both** subscribe seams consult — `EventsHub.SubscribeSession` (denial is a `HubException`; the caller is neither joined to the group nor registered in `EventsHubViewerRegistry`, so the mirror stays off) and `SessionDetailsPage`'s in-process subscribe (inline denial, no subscription) — so neither path is the weaker one and the `TODO(per-session-acl)` is gone. Decision order, fail-closed on every branch: authenticated Administrator → allow (evaluated **before** the registry lookup, so Admin × unknown-session allows — pinned by a test because reordering the two checks is a plausible refactor); session not found → deny; untagged session → `Dashboard:UntaggedSessionVisibility` (`AdminOnly` default); else allow iff `session.Tags ∩ zb:dashboardtag` claims, ordinal-ignore-case. Session tags are inherited from the owning API key's `dashboard_tags` constraint (`apikey --dashboard-tags`, already in the `ApiKeyConstraints` JSON blob — no SQLite migration) and never from the client's wire request. Viewer grants come from `Dashboard:GroupToTag` over the user's LDAP groups, stamped at cookie login (`DashboardAuthenticator.CreatePrincipal`) and **re-resolved, not copied**, at hub-token mint (`HubTokenService.Issue`), so the 5-minute token lifetime bounds a stale grant. Anonymous localhost is an empty-grant Viewer; `Dashboard:DisableLogin` auto-login carries both roles and so takes the admin bypass unchanged. Tests: `DashboardSessionAclTests` (decision table, every branch asserted in its denying direction too), `EventsHubTests`, `DashboardAuthenticatorTests`, `HubTokenServiceTests`, a `GatewayOptionsTests` case proving `Dashboard:GroupToTag` keeps its ordinal-ignore-case lookup through configuration binding, and two `[LiveLdapFact]`s in `DashboardLdapLiveTests` that drive a real bind against the shared GLAuth (`gw-viewer` → `team-a` grant admits the `team-a` session and refuses the `team-b` one; `multi-role` bypasses on the sessions its own grant does not cover). The live pair needed **no GLAuth change** — the tag layer is config-side, keyed on the existing `GwAdmin`/`GwReader` groups (recorded in `glauth.md`). Docs: `docs/Sessions.md` (session-tag model), `gateway.md` + CLAUDE.md dashboard-auth paragraphs, `docs/GatewayDashboardDesign.md` (three passages that described the ACL as outstanding), `docs/GatewayConfiguration.md` (`ShowTagValues` row: redaction is now the second of two layers, not the only one), `docs/Authentication.md` (`--dashboard-tags` is the only *constraint* flag that splits on commas). |