# Language Clients — Architecture Review (Re-run) **Date:** 2026-07-12 · **Commit:** `4f5371f` (main) · **Baseline:** `59856b8` (pre-remediation) · **Method:** static review on macOS (no builds, no tests run) Scope: `clients/dotnet` (lib + CLI + tests), `clients/go` (mxgw-go), `clients/java`, `clients/python`, `clients/rust` (lib + CLI + build.rs + vendored protos), cross-client parity, `docs/ClientPackaging.md`, client READMEs, `scripts/pack-clients.ps1`. Generated directories checked for hygiene/freshness only. Prior findings tracked as CLI-01..CLI-34 in `archreview/remediation/50-clients.md` and `archreview/remediation/00-tracking.md`. ## Prior-finding verification | ID | Claimed status | Verified? | Evidence (file:line at HEAD) | Notes | |----|----------------|-----------|------------------------------|-------| | CLI-01 | Done | **Yes** | `clients/go/mxgateway/session.go:1085-1103` (reserve check `len(results) >= eventBufferSize`, cancel + non-blocking terminal send), `:47-56` (`eventBufferSize`/`eventBufferReservedSlots`), `clients/go/mxgateway/errors.go:73` (`ErrSlowConsumer`), test `clients/go/mxgateway/client_session_test.go:150-185` | Sound: sole-producer invariant means the 17th slot is only ever used by the terminal error; doc comments on `Events`/`EventsAfter` updated. Nit: if `Recv` returns a real terminal error while the 16 data slots are full, it is reported as `ErrSlowConsumer` instead (see CLI-44). | | CLI-02 | Done | **Yes (code); docs half missing** | `clients/rust/build.rs:18-26` (repo-path-first, vendored fallback), `clients/rust/Cargo.toml:20` (`include = [... "protos/*.proto" ...]`), `clients/rust/protos/*.proto` byte-identical to `src/ZB.MOM.WW.MxGateway.Contracts/Protos/*` (diff clean), `scripts/pack-clients.ps1:190-212` (`--no-verify` removed from both `cargo package` and `cargo publish`), drift guard `scripts/check-codegen.ps1:68-82` | Core fix verified. But no doc anywhere describes the vendored layout — `clients/rust/README.md:21-22` still says build.rs reads only `../../src/.../Protos`, and `docs/ClientPackaging.md` never mentions `clients/rust/protos/` (CLI-42). | | CLI-03 | Done | **Yes, with a caveat** | `clients/rust/src/error.rs:324-333` (`ensure_mxaccess_success`, `hresult < 0`), `:104-107` (`Error::MxAccess(Box)`), wired into every `invoke` at `clients/rust/src/client.rs:296-297` (raw path preserved as `invoke_raw`); tests `error.rs:410-452` | Rust correctly adopted `hresult < 0` from the start. Caveat: the status-array check branches on `success == 0`, contradicting the proto's own "branch on category" instruction (CLI-37). | | CLI-04 | Done | **Yes (library surface, all five)** | .NET `clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewaySession.cs:891` (AdviseSupervisory), `:935` (AddBufferedItem), `:988` (SetBufferedUpdateInterval), `:1033`/`:1076` (Suspend/Activate), `:1128`/`:1200` (WriteSecured/2), `:1276` (AuthenticateUser), `:1332` (ArchestrAUserToId). Go `clients/go/mxgateway/session.go:711-996`. Java `clients/java/.../client/MxGatewaySession.java:720-988`. Python `clients/python/src/zb_mom_ww_mxgateway/session.py:595-813`. Rust `clients/rust/src/session.rs:264-925`. Per-client tests exist (e.g. `session_parity_helpers_test.go:15-199`, `test_typed_command_helpers.py`, `MxGatewayClientSessionTests.java:579-689`, `MxGatewayClientSessionTests.cs:570+`, `client_behavior.rs:572+`) | Phase 1 + Phase 2 both landed in every client. Divergences inside the new surface: HRESULT semantics differ per language (CLI-08 open → CLI-38), redaction strength differs (CLI-40), malformed-reply fallback differs (CLI-41), CLI subcommand coverage and flag names differ (CLI-45, parity note). Parity contract (WriteSecured native failure surfaced unchanged) is explicitly documented and tested in each client. | | CLI-30 | Done | **Yes** | Rust `clients/rust/src/session.rs:145-151` (`unregister`); .NET `MxGatewaySession.cs:857` (`UnregisterAsync`) — Go/Java/Python already had it | Matrix cell closed in all five. | | CLI-12 | Done | **Yes (named targets); residue elsewhere** | `clients/java/README.md`, `clients/java/JavaClientDesign.md`, `docs/ClientPackaging.md:193` all say Java 17 now (grep "Java 21" clean in those files) | Residue: `docs/style-guides/JavaStyleGuide.md:8` still says "Java 21 preferred" — style guides are authoritative per CLAUDE.md (CLI-43). Historical plan docs (`docs/plans/*`, `docs/ImplementationPlanClients.md:312`) also retain "Java 21" but are archival. | | CLI-15 | Done | **Yes (library); CLIs uneven** | .NET `MxEventStreamItem.cs` + `MxEventStreamExtensions.cs` + `MxGatewaySession.cs:1423` (`StreamEventItemsAsync`); Go `session.go:29-60` (`EventResult.ReplayGap`/`IsReplayGap`), `:1040-1042` (sentinel promoted, `Event` cleared); Rust `client.rs` `EventItem::{Event,ReplayGap}` + `from_event`; Python `events.py:11-63` (`ReplayGap` dataclass) + `session.py:816-870` (`stream_events` yields the union, `_surface_replay_gaps` propagates `aclose`); Java `MxEventStreamItem.java` + `MxEventStream.java:127` (`nextItem()`). Tests in all five (e.g. `client_behavior.rs:167-212`, `test_replay_gap.py`, `MxGatewayClientSessionTests.cs:232-270`, `MxGatewayClientSessionTests.java:546-547`, Go `client_session_test.go`). Docs: `docs/ClientLibrariesDesign.md:77-87`, `docs/CrossLanguageSmokeMatrix.md:33-39`, all five READMEs mention the gap | Uniform resume contract (`oldest_available_sequence - 1`) documented everywhere; sentinel never synthesized or swallowed by any library. **However** two CLIs mishandle it: `mxgw-py stream-events` crashes on a gap (CLI-35) and `mxgw-go stream-events` destroys it (CLI-36). Also note Java/.NET keep the typed path opt-in — the pre-existing raw iterators (`MxEventStream.next()`, `StreamEventsAsync`) still deliver the sentinel as a raw event (documented, `MxEventStream.java:28-38`). | | CLI-16 | Done | **Yes** | `docs/ClientPackaging.md:51-52` (`.slnx`), `:160` (`src/zb_mom_ww_mxgateway/generated`), `:187` (`python -m zb_mom_ww_mxgateway_cli`), `:193` (Java 17 + correct package names) | Verified against `clients/python/pyproject.toml` and the actual `.slnx`. | | CLI-18 | Done | **Yes** | `clients/dotnet/ZB.MOM.WW.MxGateway.Client/ZB.MOM.WW.MxGateway.Client.csproj:22` (`0.1.2`) | See CLI-39: 0.1.2 is the *already-published* version. | | CLI-21 | Done | **Yes (constant); guard not implemented** | `clients/go/mxgateway/version.go:5-7` (`ClientVersion = "0.1.2"`, comment "keep in sync with the module tag") | The remediation design's second half — a tag-time consistency check in `scripts/tag-go-module.ps1` — was not implemented (grep for `ClientVersion`/`version.go` in the script: no hits). Drift can recur silently. | | CLI-26 | Done | **Yes (literal)** | `clients/python/src/zb_mom_ww_mxgateway/version.py:3` (`__version__ = "0.1.2"`) matches `pyproject.toml:9` | Implemented as a second literal, not the recommended `importlib.metadata` derivation; CLI tests (`test_cli.py:213`) assert self-consistency against `__version__`, not against `pyproject.toml`, so the original drift mode remains possible. | | CLI-29 | Done | **Yes** | `clients/rust/src/version.rs:6-8` (`CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION")`) | Cannot drift by construction — the strongest of the four version fixes. | | CLI-05 | Not started | **Still open** | `MxGatewaySession.cs:19` (`internal MxGatewaySession(`); no `AttachSession` anywhere in the .NET client | .NET remains the only client that cannot re-attach a typed session wrapper — now more consequential because `ReplayGap` handling shipped and reconnect is a first-class flow. | | CLI-06 | Not started | **Still open** | `MxGatewaySession.cs:1435-1439` (`DisposeAsync` → unbounded `CloseAsync()`) | Unchanged. | | CLI-07 | Not started | **Still open** | `MxGatewayClient.cs:311` (`timeout.CancelAfter(Options.DefaultCallTimeout)` still caps the whole pipeline at one attempt's budget) | Unchanged. | | CLI-08 | Not started | **Still open — and now embedded in the new typed surface** | .NET `MxCommandReplyExtensions.cs:34` (`Hresult != 0`), Go `errors.go:187` (`GetHresult() != 0`), Java `MxGatewayErrors.java:50` (`getHresult() != 0`); Python `errors.py:133` and Rust `error.rs:325` use the correct `< 0` | Every new CLI-04 helper in .NET/Go/Java inherits `!= 0`; the same `S_FALSE`-style reply now errors in three clients and passes in two. `docs/ClientLibrariesDesign.md:118-119` incorrectly claims all five run `< 0` (CLI-38). | | CLI-09 | Not started | **Still open** | `clients/go/mxgateway/errors.go` — no `AuthenticationError`/`AuthorizationError`/sentinels (grep clean) | Unchanged. | | CLI-10 | Not started | **Still open** | `clients/go/mxgateway/client.go:64` (`grpc.WithBlock()`), `:68` (`grpc.DialContext`) | Unchanged. | | CLI-11 | Not started | **Still open** | `clients/go/cmd/mxgw-go/main.go` — no `require-certificate-validation` flag (grep clean) | Unchanged; new `write-secured`/`authenticate-user` subcommands also run skip-verify under `--tls` without CA. | | CLI-13 | Not started | **Still open** | `clients/java/.../client/MxGatewayClient.java:248` (`new MxEventStream(16)`), overflow still cancels | Unchanged. | | CLI-14 | Not started | **Still open (partially improved)** | `clients/python/src/zb_mom_ww_mxgateway/options.py:135-154` — TOFU pin + `localhost` SNI default, still no runtime warning; README now documents trust-on-first-use and `--require-certificate-validation` (`clients/python/README.md:382`, `:422`) | The doc half improved; the warning/log half not done. | | CLI-17 | Not started | **Still open** | .NET `MxGatewayClient.cs:367-370` accept-all callback; Go `client.go` `InsecureSkipVerify`; Java `InsecureTrustManagerFactory`; Python TOFU; Rust strict | Divergence unchanged; no warnings added anywhere. | | CLI-19 | Not started | **Still open** | `Properties/AssemblyInfo.cs:3` and `ZB.MOM.WW.MxGateway.Client.csproj:37` both declare `InternalsVisibleTo` | Unchanged. | | CLI-20 | Not started | **Still open** | `MxGatewayClient.cs:367-370` — accept-all callback installed silently | Unchanged. | | CLI-22 | Not started | **Still open** | `clients/go/mxgateway/session.go:1126-1131` (`newCorrelationID` returns `""` on `rand.Read` error) | Unchanged; every new typed helper routes through it. | | CLI-23 | Not started | **Still open** | `session.go:433-441` (`WriteBulk` short-circuits `len==0`) vs `:289-309` (`AddItemBulk` sends empty to the wire) | Both now reject `nil` explicitly, but the empty-slice asymmetry persists. | | CLI-24 | Not started | **Incidentally FIXED** | `clients/java/.../client/MxEventStream.java:25-28` — "Single consumer. This stream is not safe to drain from more than one thread." | Landed as part of the CLI-15 Javadoc work. Tracking should be updated to Done. | | CLI-25 | Not started | **Still open** | `MxGatewayClient.java:347-351` (`close()` calls `shutdown()` without awaiting) | Unchanged. | | CLI-27 | Not started | **Still open** | `clients/python/src/zb_mom_ww_mxgateway/session.py:40-48` (no lock; repeated close synthesizes a local `CloseSessionReply`) | Unchanged. | | CLI-28 | Not started | **Still open** | `session.py:913` (`from .client import GatewayClient # noqa: E402` still at the bottom) | Unchanged. | | CLI-31 | Not started | **Still open — worse** | `clients/rust/crates/mxgw-cli/src/main.rs` is now 2,889 lines (was 2,699) | The three new subcommands grew the monolith. | | CLI-32 | Not started | **Still open** | No bulk cap in `MxGatewaySession.cs` or `MxGatewaySession.java` (grep for 1000/MAX_BULK clean); Go/Python/Rust caps intact (`session.go:19`, `session.py:11`, `session.rs:29`) | Unchanged. | | CLI-33 | Not started | **Still open** | `docs/ClientBehaviorFixtures.md` — no backpressure/slow-consumer section (grep clean) | The Go behavior it would document has changed (CLI-01), making the doc gap slightly more visible; behavior is at least documented in Go doc comments and README. | | CLI-34 | Not started | **Resolved (covered)** | Root `.gitignore:87` (`.pytest_cache/`), `:103` (`**/build/`); `git check-ignore` confirms both `clients/python/build` and `clients/python/.pytest_cache` are ignored | No accidental-commit risk. Tracking can be closed. | **Done claims that don't fully hold:** none of the Done claims is false at the library level, but three carry material caveats: (1) **CLI-15** — the Python and Go *CLIs* were not updated for the typed gap and now misbehave on it (new findings CLI-35/CLI-36); (2) **CLI-02** — the documentation half (vendored-proto layout in `clients/rust/README.md` / `docs/ClientPackaging.md`) was skipped; (3) the tracking change-log entry for CLI-04 ("each runs `hresult < 0` + `MxStatusProxy` validation") and `docs/ClientLibrariesDesign.md:118-119` overstate — .NET/Go/Java still validate `!= 0` (CLI-08 is open, as tracked). CLI-21 and CLI-26 are fixed as constants but without the drift guards their designs specified. ## New findings ### CLI-35 — Python CLI `stream-events` crashes on a ReplayGap **Severity:** Medium · **Category:** correctness / CLI **Files:** `clients/python/src/zb_mom_ww_mxgateway_cli/commands.py:1098-1106` (`_stream_events`), `:1627-1632` (`_message_dict`); `clients/python/src/zb_mom_ww_mxgateway/session.py:816-870` `Session.stream_events()` now yields `pb.MxEvent | ReplayGap` (the CLI-15 change), but the CLI's `_stream_events` feeds every collected item into `_message_dict`, which calls `google.protobuf.json_format.MessageToDict`. `ReplayGap` is a plain dataclass (`events.py:11`), not a proto message, so `MessageToDict` raises and the command exits with an error after having consumed the stream — precisely on the resume-after-outage path where an operator most needs the tool. No test covers it (`grep -i replay clients/python/tests/test_cli.py` → nothing). **Recommendation:** branch on `isinstance(item, ReplayGap)` in `_stream_events` and emit a distinct JSON object (mirror the Rust CLI's `{"replayGap": {...}}` row, `mxgw-cli/src/main.rs:1020-1035`); add a CLI test that scripts a gap. ### CLI-36 — Go CLI `stream-events` silently destroys the ReplayGap signal **Severity:** Medium · **Category:** correctness / CLI **Files:** `clients/go/cmd/mxgw-go/main.go:969-983`; `clients/go/mxgateway/session.go:1040-1042` The library deliberately clears `EventResult.Event` on a gap (`session.go:1041`, "so consumers never process the sentinel as a data change"), but the CLI loop only checks `result.Err` and then formats `result.Event`: JSON mode marshals a nil `*MxEvent` (empty object), text mode prints `0 MX_EVENT_FAMILY_UNSPECIFIED`. The gap's cursors (`requested_after_sequence`, `oldest_available_sequence`) — the exact data an operator needs to resume — are discarded. Contrast: the Rust CLI prints a dedicated `REPLAY_GAP` row; the .NET and Java CLIs pass the raw sentinel through proto-JSON so `replayGap` is at least visible. **Recommendation:** add an `if result.IsReplayGap()` branch printing the two sequences (text) / a `replayGap` JSON object; add a CLI test. ### CLI-37 — Status-array validation branches on `success`, contradicting the contract's own "branch on category" rule; .NET is the lone divergent (and lone conformant) client **Severity:** Medium · **Category:** correctness / cross-client parity **Files:** `src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto:982-992`; Rust `clients/rust/src/error.rs:326` (`status.success == 0`); Go `clients/go/mxgateway/status.go:4-6` (`GetSuccess() != 0`); Java `MxStatuses.java:26`; Python `errors.py:141`; .NET `MxStatusProxyExtensions.cs:11-17` (`Success != 0 && Category is MxStatusCategory.Ok`) The proto comment (pre-dating remediation) states `success` "is NOT a boolean … carried verbatim for diagnostics; the authoritative success/failure of the operation is `category` (MX_STATUS_CATEGORY_OK marks success) … Clients should branch on `category`, not on a specific `success` value." Four clients — including the brand-new Rust `ensure_mxaccess_success` written for CLI-03 — branch only on `success`; .NET requires both `success != 0` *and* `category == Ok`. A reply with `success = 1, category = COMMUNICATION_ERROR` passes Go/Java/Python/Rust and throws in .NET; a reply with `success = 0, category = OK` does the reverse in .NET. Either the proto comment is wrong (then fix the comment and .NET) or the four clients are (then fix them) — today the wire contract and four of five implementations disagree. **Recommendation:** decide the authoritative field once (the proto says `category`), align all five `IsSuccess`-equivalents, and add a shared behavior-fixture case (`success` and `category` disagreeing) so the suites lock it in. Coordinate with CLI-08 since it is the same validation function in every client. ### CLI-38 — Shared design doc claims `hresult < 0` validation that three clients don't perform **Severity:** Medium · **Category:** documentation drift (CLAUDE.md same-commit rule) **Files:** `docs/ClientLibrariesDesign.md:118-119` ("runs the same MXAccess-level reply validation (HRESULT `< 0` + per-item `MxStatusProxy`)"); actual: `MxCommandReplyExtensions.cs:34`, `errors.go:187`, `MxGatewayErrors.java:50` all `!= 0` The Typed Command Parity section (added with CLI-04) documents COM-correct `< 0` semantics as if uniform, but only Python and Rust implement them; the 00-tracking change log (2026-07-09 Wave E2 entry) repeats the same claim. Until CLI-08 lands, the load-bearing design doc describes behavior that .NET/Go/Java do not have, and a positive success HRESULT (e.g. `S_FALSE`) on any *new* helper (WriteSecured, AuthenticateUser, …) errors in three languages and succeeds in two. **Recommendation:** either land CLI-08 (three one-line changes + tests, already designed) or correct the doc to state the current divergence. Landing CLI-08 is strictly better and closes CLI-38 for free. ### CLI-39 — Version constants aligned to the already-published 0.1.2 despite breaking API changes since that publish **Severity:** Medium · **Category:** packaging / release hygiene **Files:** `clients/rust/Cargo.toml:3` (0.1.2), `clients/python/pyproject.toml:9` + `version.py:3` (0.1.2), `clients/go/mxgateway/version.go:7` (0.1.2), `clients/dotnet/.../ZB.MOM.WW.MxGateway.Client.csproj:22` (0.1.2) 0.1.2 was published to Gitea before this remediation (per `scripts/pack-clients.ps1` history). Since then the public API changed incompatibly in at least two clients: Rust's `EventStream` item type changed from `Result` to `Result` (`clients/rust/src/client.rs:139-141`) and `Error` gained a variant; Python's `stream_events` now yields `MxEvent | ReplayGap` (`session.py:816-820`), breaking consumers that assume proto messages. The version-alignment fixes (CLI-18/21/26) set the source to the *old* released number instead of bumping. Consequence: the next `cargo publish` / `pip` publish of the current tree either collides with the existing 0.1.2 artifact (registries reject duplicate versions) or, if forced, ships a different API under an identical version string. **Recommendation:** bump all five clients to 0.1.3 (or 0.2.0 for Rust/Python given the breaking stream-surface change; Java already sits at 0.2.0) before the next `pack-clients.ps1` run, and add the version-vs-registry check to the pack script. ### CLI-40 — Credential-redaction seam is uniform in name only; Rust/Java/.NET cannot scrub an echoed password **Severity:** Low · **Category:** security / cross-client parity **Files:** Go `clients/go/mxgateway/errors.go:16-66` + `session.go:745`, `:836` (exact-secret `redactSecrets` on WriteSecured value + AuthenticateUser password); Python `session.py:573-592` + `:875-909` (exact-secret `_invoke_redacted`); Rust `clients/rust/src/error.rs:363-375` (`redact_credentials` scrubs only whitespace-delimited tokens starting `mxgw_` or equal to `bearer`); Java `MxGatewaySecrets.java:44-57` (same pattern-only scrub); .NET — no library-level scrub at all (exception text embeds `status.DiagnosticText` verbatim via `MxStatusProxyExtensions.ToDiagnosticSummary`; only the CLI redacts, `MxGatewayCliSecretRedactor.cs:14-32`) The design doc (`docs/ClientLibrariesDesign.md:123-127`) and the Rust helper docs (`session.rs:876-883`: diagnostics "are scrubbed by the client's credential-redaction seam") claim credentials can never reach exception text. That holds by *construction* everywhere (exceptions carry reply-derived text, never the request), but only Go and Python defend against the gateway/MXAccess echoing the credential back in `diagnostic_text`; Rust/Java scrub only API-key-shaped tokens and .NET scrubs nothing in the library. The per-client tests pass because the fakes don't echo. **Recommendation:** either downgrade the doc claim to "requests are never embedded in errors" or port Go's exact-secret scrub to Rust (`MxAccessError::Display` already funnels through `redact_credentials` — extend it to take known secrets), Java, and .NET. Add one shared fixture where the scripted reply's `diagnostic_text` contains the credential. ### CLI-41 — Malformed-reply fallback for `AuthenticateUser`/`ArchestrAUserToId`/`AddBufferedItem` differs per language (silent 0 vs typed error vs NRE) **Severity:** Low · **Category:** cross-client parity / robustness **Files:** Go `session.go:807-819` (falls back to `GetReturnValue().GetInt32Value()` → silent `0` when both absent); Python `session.py:707` (`reply.authenticate_user.user_id` → proto3 default `0` when payload absent, no fallback, no error); Java `MxGatewaySession.java:868-880` (payload else `getReturnValue().getInt32Value()` → `0`); .NET `MxGatewaySession.cs:1289` (`reply.AuthenticateUser?.UserId ?? reply.ReturnValue.Int32Value` — `NullReferenceException` when both are absent, since `ReturnValue` is a nullable message); Rust `session.rs` helpers `authenticate_user_id`/`archestra_user_id` return `Error::MalformedReply` (`session.rs:1073-1089`), while `add_buffered_item_handle` *does* fall back to `return_value` (`:1057-1071`) — inconsistent even within Rust A gateway reply that omits the typed payload yields: user id `0` (Go/Java, and Python even when `return_value` is present), an NRE (.NET), or a typed error (Rust). Getting `0` back from `AuthenticateUser` is the worst mode — callers feed it into `WriteSecured` as a real user id. **Recommendation:** pick one contract (Rust's `MalformedReply`-style error is the safest) and apply it to all five; at minimum make Python check `HasField`/`WhichOneof` and .NET null-check `ReturnValue`. ### CLI-42 — CLI-02's documentation half missing: vendored Rust protos undescribed anywhere **Severity:** Low · **Category:** documentation drift **Files:** `clients/rust/README.md:21-22` (still: "build.rs reads the `.proto` files from `../../src/ZB.MOM.WW.MxGateway.Contracts/Protos`" — no mention of the `clients/rust/protos/` fallback); `docs/ClientPackaging.md` (no "vendor"/`protos/` mention; grep `-i vendored docs/*.md` → zero hits repo-wide) The vendored copies are load-bearing published build inputs with a refresh obligation on every `.proto` change (enforced only by `scripts/check-codegen.ps1` Check 3 and a `build.rs` comment). CLAUDE.md's docs-in-same-commit rule was not met for CLI-02. **Recommendation:** one paragraph in the Rust README's generation section + the Rust section of `docs/ClientPackaging.md` describing the repo-path-first/vendored-fallback resolution and the refresh rule. ### CLI-43 — Java style guide still prescribes "Java 21 preferred" after the 17 retarget **Severity:** Low · **Category:** documentation drift **Files:** `docs/style-guides/JavaStyleGuide.md:8` CLAUDE.md declares `docs/style-guides/` authoritative; the guide contradicts the shipped `toolchain 17` / `options.release = 17` build and the corrected CLI-12 docs. (Historical plan docs `docs/plans/2026-05-28-*.md:9`, `docs/plans/2026-06-16-*.md:9`, `docs/ImplementationPlanClients.md:312` also say 21, but are archival.) **Recommendation:** update the style guide line to Java 17 (Ignition 8.3 target), optionally noting 21+ compatibility. ### CLI-44 — Go event goroutine can mislabel a genuine terminal stream error as `ErrSlowConsumer` **Severity:** Low · **Category:** correctness (edge case) **Files:** `clients/go/mxgateway/session.go:1051-1057` (Recv-error path reuses `sendEventResult`) + `:1085-1100` (overflow branch) When `stream.Recv()` returns a real terminal error while the 16 data slots are full, `sendEventResult` hits the overflow branch first and enqueues `ErrSlowConsumer` instead of the actual `GatewayError{Err: err}` — the consumer gets a loud terminal error (good) with the wrong root cause (mildly misleading; the real gRPC status is lost). Not silent loss, so Low. **Recommendation:** in the Recv-error path, bypass the overflow substitution (e.g. a `terminal bool` parameter that uses the reserved slot directly with the caller's error). ### CLI-45 — New CLI credential flags diverge: env-var names, empty-password handling, and missing-password behavior differ per language **Severity:** Low · **Category:** cross-client parity / CLI usability **Files:** Go `cmd/mxgw-go/main.go:441-443` (default env `MXGATEWAY_VERIFY_PASSWORD`; empty resolved password sent to the wire); Java `MxGatewayCli.java:1140-1160` (same env name; `resolvedPassword = ""` fallback sent as empty credential); Rust `mxgw-cli/src/main.rs:143-152` (same env name; missing → `Error::InvalidArgument`); Python `commands.py:833-848` (no default env name — must pass `--password-env`; missing → `click.UsageError`); .NET `MxGatewayClientCli.cs:355-388` (different env name `MXGATEWAY_VERIFY_USER_PASSWORD` and flag `--verify-user-password`; missing → `ArgumentException`) The same operator workflow (`export MXGATEWAY_VERIFY_PASSWORD=… && authenticate-user …`) works in Go/Java/Rust, needs an explicit flag in Python, and needs a *different* variable in .NET; Go and Java silently authenticate with an empty password where the other three fail fast. Subcommand coverage also diverges: .NET CLI exposes all nine new commands (unregister/buffered/suspend/activate/write-secured2/archestra-user-to-id), Rust adds unregister + the two credential commands, Go/Python/Java add only `write-secured` + `authenticate-user`. **Recommendation:** standardize on one env-var name (and add it as an alias where it differs), fail fast on empty passwords in Go/Java, and either level up the CLI subcommand sets or note the deltas in `docs/CrossLanguageSmokeMatrix.md`. ## Severity roll-up (new findings) | Severity | Count | IDs | |---|---|---| | High | 0 | — | | Medium | 5 | CLI-35, CLI-36, CLI-37, CLI-38, CLI-39 | | Low | 6 | CLI-40, CLI-41, CLI-42, CLI-43, CLI-44, CLI-45 | ## Cross-client parity note (updated) The two headline parity gaps from the prior review are closed at the library level: **typed single-item command parity** (CLI-04, incl. the full buffered/suspend/activate family and `Unregister` everywhere) and the **typed `ReplayGap` signal** (CLI-15) now exist in all five clients with tests, and the Go overflow path is loud (CLI-01). What remains divergent: | Behavior | .NET | Go | Java | Python | Rust | |---|---|---|---|---|---| | HRESULT failure test (CLI-08) | `!= 0` | `!= 0` | `!= 0` | `< 0` ✓ | `< 0` ✓ | | Status-entry failure test (CLI-37) | `success==0` **or** `category != Ok` | `success==0` | `success==0` | `success==0` | `success==0` | | ReplayGap in default stream surface | opt-in (`StreamEventItemsAsync`) | typed by default (`EventResult`) | opt-in (`nextItem()`) | typed by default | typed by default (`EventItem`) | | CLI renders ReplayGap | raw proto JSON (visible) | **lost** (CLI-36) | raw proto JSON (visible) | **crash** (CLI-35) | dedicated row ✓ | | Credential scrub of surfaced errors (CLI-40) | none (lib) / exact (CLI) | exact-secret | `mxgw_`/`bearer` pattern | exact-secret | `mxgw_`/`bearer` pattern | | AuthenticateUser malformed-reply result (CLI-41) | NRE possible | silent 0 | silent 0 | silent 0 | typed error ✓ | | Session re-attach to existing id (CLI-05) | **No** | Yes | Yes | Yes | Yes | | Client-side 1000-item bulk cap (CLI-32) | No | Yes | No | Yes | Yes | | Event backpressure (CLI-01/13/33) | unbuffered | 16+1, loud `ErrSlowConsumer` ✓ | 16, cancel+error | unbuffered | unbuffered | | TLS default (CLI-17) | accept-all | accept-all | accept-all | TOFU pin | strict pin | | Typed auth errors (CLI-09) | Yes | **No** | Yes | Yes | Yes | | Version constant vs published artifact (CLI-39) | 0.1.2 = already-shipped | 0.1.2 = already-shipped | 0.2.0 | 0.1.2 = already-shipped | 0.1.2 = already-shipped | | Credential CLI env var (CLI-45) | `MXGATEWAY_VERIFY_USER_PASSWORD` | `MXGATEWAY_VERIFY_PASSWORD` | `MXGATEWAY_VERIFY_PASSWORD` | none (explicit flag) | `MXGATEWAY_VERIFY_PASSWORD` | The resume contract itself (`after_worker_sequence = oldest_available_sequence - 1`) is uniform and consistently documented in all five clients, both proto comments, the design doc, and the smoke matrix — that part of the epic is genuinely converged. ## Still good / preserved - The remediation work is high quality where it landed: the Go reserved-slot design is provably safe (single producer), the Rust vendored-proto fallback keeps in-repo edits live while making the tarball self-sufficient, `cargo package` verification is restored, and `check-codegen.ps1` Check 3 guards vendored-proto drift. - MXAccess parity discipline held under pressure: every new secured/auth helper documents and tests that native failures (WriteSecured before authenticate/advise, failed authentication) surface unchanged; no client synthesizes or swallows events, and the ReplayGap sentinel is forwarded, never invented (`EventItem::from_event`, `_surface_replay_gaps`, `MxEventStreamItem`, Go's cleared-`Event` conversion all preserve this). - Generated-code hygiene remains clean: vendored Rust protos byte-identical to Contracts; the Java `MxaccessWorker.java` regen matches a real `mxaccess_worker.proto` change (+6 lines) rather than spurious churn; the tracked Java protoset descriptor was refreshed in step; a `checkGeneratedClean` Gradle guard (build.gradle:68+) now catches stale Java codegen; Go/Python `generate-proto.ps1` scripts are now PATH-portable with pinned-version assertions instead of hard-coded per-machine paths. - The non-doc .NET library diff outside the new features is pure XML-doc completion (verified: no behavioral lines). - Test investment matches the surface: ~1,500 new test lines across the five suites, including per-client credential-absence assertions, replay-gap fixtures, the Go slow-consumer test, and Rust ensure_mxaccess_success unit tests.