Six-domain re-review at the P2 merge (4f5371f): all prior Done claims
verified (none false, 10 partial), 47 new findings (1 High, 14 Medium),
with per-finding design/implementation plans and a new tracking register.
30 KiB
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<MxAccessError>)), 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 (<Version>0.1.2</Version>) |
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<MxEvent, Error> to Result<EventItem, Error> (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=… && <cli> 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 packageverification is restored, andcheck-codegen.ps1Check 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-Eventconversion all preserve this). - Generated-code hygiene remains clean: vendored Rust protos byte-identical to Contracts; the Java
MxaccessWorker.javaregen matches a realmxaccess_worker.protochange (+6 lines) rather than spurious churn; the tracked Java protoset descriptor was refreshed in step; acheckGeneratedCleanGradle guard (build.gradle:68+) now catches stale Java codegen; Go/Pythongenerate-proto.ps1scripts 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.