Files
mxaccessgw/archreview/remediation/50-clients.md
T
Joseph Doherty 59856b8c63 docs(archreview): add architecture review + per-domain remediation designs and tracker
Adds the 2026-07-08 architecture review (00-overall + six domain reports)
and a remediation/ tree: one design+implementation doc per domain covering
every finding, plus 00-tracking.md as the master progress tracker.

- 153 findings with stable IDs (GWC/WRK/IPC/SEC/CLI/TST), each with
  design rationale, implementation steps, tests, docs, and verification.
- Tracker rolls findings up by severity and P0/P1/P2 roadmap tier, records
  cross-cutting clusters and per-finding status (all Not started).
- Planning docs only; no source changes.
2026-07-09 00:39:00 -04:00

56 KiB
Raw Blame History

Language Clients — Remediation Design & Implementation

Source review: 50-clients.md · Generated: 2026-07-09

This document turns the Language Clients review into buildable remediation entries for all five clients (clients/dotnet, clients/go, clients/java, clients/python, clients/rust). Every finding carries the target language explicitly. The JDK 17 retarget is verified correct in build config (J1 in the source report is a no-defect verification and gets no remediation row); its only fallout is stale docs (CLI-12). Two operational constraints from prior work carry into these fixes: the Java client cannot build or test on the macOS tree (no local JRE — build/test on windev), and the Java Gradle build regenerates a tracked ~64k-line MxaccessGateway.java whose spurious protobuf-version churn must be reverted (git checkout) whenever no .proto changed. Python regen must pin grpcio-tools to the baseline (grpcio 1.80.0 / protobuf 6.31.1); the Rust build.rs proto path and --no-verify packaging are themselves a finding (CLI-02).

Finding index

ID Sev Title Roadmap Effort Depends on Files
CLI-01 High Go Session.Events() silently closes stream on 16-slot overflow P0 M clients/go/mxgateway/session.go
CLI-02 High Rust crate unbuildable outside the repo (build.rs path + --no-verify) P1 M clients/rust/build.rs, clients/rust/Cargo.toml, scripts/pack-clients.ps1
CLI-03 High Rust invoke never validates HRESULT / MXSTATUS_PROXY P0 M clients/rust/src/error.rs, clients/rust/src/client.rs
CLI-04 High Typed-command parity gap across all five clients (WriteSecured/AuthenticateUser/AdviseSupervisory/buffered) P2 L CLI-15 clients/{dotnet,go,java,python,rust} session surfaces
CLI-05 Medium .NET session cannot be re-attached to an existing session id S clients/dotnet/.../MxGatewaySession.cs, MxGatewayClient.cs
CLI-06 Medium .NET DisposeAsync blocks/throws on unreachable gateway S clients/dotnet/.../MxGatewaySession.cs
CLI-07 Medium .NET retry budget self-defeats on DeadlineExceeded S clients/dotnet/.../MxGatewayClient.cs, MxGatewayClientRetryPolicy.cs
CLI-08 Medium .NET/Go/Java treat any nonzero HRESULT as failure (should be < 0) S CLI-03 clients/dotnet/.../MxCommandReplyExtensions.cs, clients/go/mxgateway/errors.go, clients/java/.../MxGatewayErrors.java
CLI-09 Medium Go has no typed auth-error mapping (Unauthenticated vs PermissionDenied) M clients/go/mxgateway/errors.go, client.go
CLI-10 Medium Go uses deprecated grpc.DialContext + grpc.WithBlock() M clients/go/mxgateway/client.go
CLI-11 Medium Go CLI cannot opt into strict TLS validation S clients/go/cmd/mxgw-go/main.go
CLI-12 Medium Java docs still say "Java 21" after the JDK 17 retarget P2 S clients/java/README.md, clients/java/JavaClientDesign.md, docs/ClientPackaging.md
CLI-13 Medium Java event-stream buffer hardcoded 16, cancel-on-overflow M clients/java/.../MxGatewayClient.java, MxEventStream.java
CLI-14 Medium Python default TLS is blocking TOFU pin with silent localhost SNI S clients/python/.../options.py, clients/python/README
CLI-15 Medium No client handles ReplayGap or offers a reconnect helper P2 M clients/{dotnet,go,java,python,rust} + READMEs
CLI-16 Medium docs/ClientPackaging.md drifted from Python naming and .NET .slnx P2 S CLI-12 docs/ClientPackaging.md, docs/ClientLibrariesDesign.md
CLI-17 Medium TLS default posture inconsistent across the five clients M CLI-14 clients/{dotnet,go,java,python,rust} TLS paths
CLI-18 Low .NET csproj records no <Version> P2 S clients/dotnet/.../ZB.MOM.WW.MxGateway.Client.csproj
CLI-19 Low .NET duplicate InternalsVisibleTo (AssemblyInfo + csproj) S clients/dotnet/.../Properties/AssemblyInfo.cs, csproj
CLI-20 Low .NET --tls without CA installs accept-all callback S CLI-17 clients/dotnet/.../MxGatewayClient.cs
CLI-21 Low Go ClientVersion = "0.1.0-dev" stale vs tagged releases P2 S clients/go/mxgateway/version.go, scripts/tag-go-module.ps1
CLI-22 Low Go newCorrelationID swallows crypto/rand error → empty id S clients/go/mxgateway/session.go
CLI-23 Low Go nil-vs-empty bulk short-circuit asymmetry S clients/go/mxgateway/session.go
CLI-24 Low Java MxEventStream single-consumer constraint undocumented S clients/java/.../MxEventStream.java
CLI-25 Low Java close() does not await channel termination S clients/java/.../MxGatewayClient.java
CLI-26 Low Python version.py (0.1.0) ≠ pyproject.toml (0.1.2) P2 S clients/python/.../version.py, pyproject.toml
CLI-27 Low Python Session.close() not concurrency-safe; synthesizes reply S clients/python/.../session.py
CLI-28 Low Python circular-import workaround at end of session.py S clients/python/.../session.py
CLI-29 Low Rust CLIENT_VERSION (0.1.0-dev) ≠ Cargo.toml (0.1.2) P2 S clients/rust/src/version.rs
CLI-30 Low Rust has no unregister typed helper S CLI-04 clients/rust/src/session.rs
CLI-31 Low Rust CLI is a single 2,699-line main.rs M clients/rust/crates/mxgw-cli/src/main.rs
CLI-32 Low Client-side bulk caps differ (.NET/Java unbounded) S clients/dotnet, clients/java session surfaces
CLI-33 Low Per-language event backpressure semantics undocumented S CLI-01, CLI-13 docs/ClientBehaviorFixtures.md
CLI-34 Low Python build//.pytest_cache/ present on disk (untracked) S clients/python/.gitignore

CLI-01 — Go Session.Events() silently closes stream on 16-slot overflow High · P0

Finding. In the Go client, subscribeEventsAfter (clients/go/mxgateway/session.go:700-742) spawns a goroutine that pushes each event through sendEventResult into a 16-slot channel. When the buffer is full and cancelWhenBufferFull is set (the Events()/EventsAfter() path), the default: arm cancels the stream and returns false with no terminal error enqueued (session.go:751-768); the goroutine then close(results) via defer (:714). The doc comment promises the stream runs "until … a terminal error is sent" (:675-677) but never mentions this drop.

Impact. A consumer that stalls for more than 16 queued events sees a closed channel indistinguishable from graceful server end. Events are lost with no signal — silent data loss, the single most serious runtime defect in the client tree. This is roadmap P0 item 6.

Design. Reserve one slot for a terminal EventResult so overflow is always observable. Add an exported sentinel ErrSlowConsumer and, on the overflow branch, cancel the stream and attempt a non-blocking send of EventResult{Err: ...wrapping ErrSlowConsumer}; the reserved capacity guarantees that final send succeeds even when the 16 data slots are full. Keep the blocking SubscribeEvents path (which rides gRPC flow control) unchanged. Rejected alternative: dropping the buffered-cancel variant entirely — it is public API and some callers depend on non-blocking semantics; a loud terminal error preserves the contract while removing the silent-loss failure mode.

Implementation.

  • clients/go/mxgateway/errors.go: add var ErrSlowConsumer = errors.New("mxgateway: event consumer fell behind; stream terminated").
  • clients/go/mxgateway/session.go: size the channel to 16+1 (or track a reserved terminal slot); in sendEventResult's default: arm, call cancel() then a non-blocking select { case results <- EventResult{Err: &GatewayError{Op: "stream events", Err: ErrSlowConsumer}}: default: } before returning false. Update the Events/EventsAfter doc comments to state the slow-consumer termination contract.
  • Tests: extend the session event tests (clients/go/mxgateway/*_test.go, the fixture-driven event suite) with a slow-consumer case asserting the channel yields a final EventResult whose errors.Is(res.Err, ErrSlowConsumer) is true before close.
  • Docs: record the Go slow-consumer terminal-error behavior in docs/ClientBehaviorFixtures.md (see CLI-33).

Verification. From clients/go: gofmt -l ., go build ./..., go test ./....


CLI-02 — Rust crate unbuildable outside the repository High · P1

Finding. The Rust build.rs resolves protos two directories above the crate — repo_root.join("src/ZB.MOM.WW.MxGateway.Contracts/Protos") with the hard assumption "clients/rust must live two levels below the repository root" (clients/rust/build.rs:8-16) — and src/generated.rs is only tonic::include_proto! of that build output. Cargo.toml does not vendor the .proto files. The packaging script hides the defect with cargo package --no-verify / cargo publish --no-verify (scripts/pack-clients.ps1:190-211). Any consumer of the published zb-mom-ww-mxgateway-client 0.1.2 fails in build.rs on first cargo build.

Impact. The Gitea-published crate is dead on arrival for every external consumer — the most serious packaging finding. This is roadmap P1 item 11.

Design. Vendor the three protos into the crate and make build.rs prefer the in-repo source but fall back to the vendored copy, then drop --no-verify so cargo package proves standalone buildability. Copy mxaccess_gateway.proto, mxaccess_worker.proto, galaxy_repository.proto into clients/rust/protos/ and list that dir in Cargo.toml's include. In build.rs, probe the repo path first (keeps in-repo edits live) and fall back to CARGO_MANIFEST_DIR/protos when the repo path is absent. The vendored copies are build inputs, not the canonical source (which remains in Contracts) — add a note so they are refreshed on .proto change. Rejected alternative: publishing pre-generated .rs — tonic's generated code is tied to the tonic/prost version and would rot; regenerating from vendored protos at build time is the idiomatic Rust approach.

Implementation.

  • Add clients/rust/protos/{mxaccess_gateway,mxaccess_worker,galaxy_repository}.proto (copies).
  • clients/rust/build.rs: let proto_root = if repo_proto_root.exists() { repo_proto_root } else { manifest_dir.join("protos") }; and compile from whichever exists; keep the descriptor-set output.
  • clients/rust/Cargo.toml: add include = ["src/**/*.rs", "protos/*.proto", "build.rs", ...] so the protos ship in the package.
  • scripts/pack-clients.ps1:190-211: remove --no-verify from the Rust cargo package/cargo publish invocations so verification runs.
  • Optional: a small build.rs staleness note or a repo script that copies Contracts protos → clients/rust/protos/ to keep them in sync (tie into the codegen-freshness theme of report 30/60).
  • Docs: update the Rust section of docs/ClientPackaging.md and clients/rust/README.md to describe the vendored-proto layout.

Verification. From clients/rust: cargo fmt, cargo check --workspace, cargo test --workspace, cargo clippy --all-targets -- -D warnings, and crucially cargo package (no --no-verify) to prove the crate builds from the packaged tarball.


CLI-03 — Rust invoke never validates HRESULT / MXSTATUS_PROXY High · P0

Finding. In the Rust client, ensure_command_success checks only protocol_status.code == Ok (clients/rust/src/error.rs:214-226) and is the sole reply check on the invoke path (clients/rust/src/client.rs:177-179). It never inspects hresult or the MXSTATUS_PROXY array. Every other client performs a second MXAccess-level check (MxCommandReplyExtensions.cs:27-41, errors.go:117-130, MxGatewayErrors.java, errors.py:122-148).

Impact. A reply with an OK protocol envelope but failing per-item MXAccess statuses reads as success in Rust — a session.write(...) can report success while MXAccess rejected the write. There is also no distinct MXAccess error variant to catch. Violates MXAccess-parity-is-the-contract. Roadmap P0 item 6.

Design. Add an ensure_mxaccess_success pass mirroring the other clients and an Error::MxAccess variant carrying the reply, and call it after ensure_command_success on the typed command paths. Use the correct COM semantics — hresult < 0 (see CLI-08), matching Python, rather than != 0; then any per-item status where success is false raises. Keep the raw invoke escape hatch un-validated (callers opting out of typed helpers accept raw replies), but apply the check in the typed session.write/command helpers. This is co-designed with CLI-08 (HRESULT semantics) — Rust should land on < 0 from the start rather than repeat the != 0 mistake.

Implementation.

  • clients/rust/src/error.rs: add Error::MxAccess(Box<MxAccessError>) (boxed, consistent with the existing boxed tonic::Status/CommandError pattern) with a thiserror message summarizing hresult + status entries and credential-safe formatting; add pub fn ensure_mxaccess_success(reply: MxCommandReply) -> Result<MxCommandReply, Error> checking hresult < 0 and each MxStatusProxy.success.
  • clients/rust/src/client.rs / session.rs: invoke ensure_mxaccess_success after ensure_command_success in the typed write/command wrappers.
  • Tests: add a Rust unit/fixture test asserting a reply with protocol_status = Ok but a failing status entry (and one with a negative hresult) yields Error::MxAccess; reuse the shared behavior fixtures if they carry such a case.
  • Docs: note the new Error::MxAccess variant in clients/rust/README.md.

Verification. From clients/rust: cargo fmt, cargo check --workspace, cargo test --workspace, cargo clippy --all-targets -- -D warnings.


CLI-04 — Typed-command parity gap across all five clients High · P2

Finding. No client (.NET, Go, Java, Python, Rust) exposes typed session helpers for single-item WriteSecured/WriteSecured2, AuthenticateUser, ArchestrAUserToId, AdviseSupervisory, AddBufferedItem, SetBufferedUpdateInterval, Suspend, or Activate, though the wire contract defines all of them (src/ZB.MOM.WW.MxGateway.Contracts/Protos/mxaccess_gateway.proto:150-160) and gateway.md documents AdviseSupervisory as a precondition for user-attributed plain writes. Only the .NET, Go, and Python CLIs offer advise-supervisory via hand-built raw commands (clients/go/cmd/mxgw-go/main.go:364-391, clients/dotnet/.../MxGatewayClientCli.cs:456-470, clients/python/.../commands.py:287-291); Java and Rust CLIs lack even that.

Impact. The secured-write parity path (AuthenticateUser → AdviseSupervisory → WriteSecured) and the buffered-event family (OnBufferedDataChange) are reachable only through raw Invoke — the single most important MXAccess parity surface is untyped in every language. Roadmap P2 item 13.

Design. Add the missing typed session helpers to all five clients, phased by parity value: (1) adviseSupervisory, single-item writeSecured/writeSecured2, authenticateUser, archestrAUserToId; (2) buffered family addBufferedItem/setBufferedUpdateInterval and suspend/activate. Each helper wraps the existing raw-Invoke machinery already used by the bulk variants, so no new wire surface is needed — the contract already carries the command kinds (mxaccess_gateway.proto:150-160). Follow each language's existing typed-helper idiom (e.g. .NET InvokeCommandAsync in MxGatewaySession.cs, Go invokeCommand, Rust session.rs wrappers, Java stub methods, Python async methods). Preserve MXAccess parity semantics exactly — do not "fix" WriteSecured requiring a prior authenticate/advise; surface the native failure. Credentials passed to authenticateUser/writeSecured must route through each client's existing secret-redaction seam (MxGatewaySecrets.java, Rust error.rs scrubbing, Go/Python redaction) so they never reach logs or error messages. Depends on CLI-15 (both touch the same session surfaces and READMEs — land as a coordinated session-surface pass).

Implementation.

  • .NET: add WriteSecuredAsync/AuthenticateUserAsync/AdviseSupervisoryAsync/etc. to MxGatewaySession.cs, mirroring the bulk-helper pattern; extend MxGatewayClientCli.cs.
  • Go: add methods to session.go alongside the bulk helpers; promote the CLI's hand-built advise-supervisory command into a typed method.
  • Java: add methods to MxGatewaySession.java and a CLI subcommand (zb-mom-ww-mxgateway-cli).
  • Python: add async methods to session.py and CLI commands in commands.py.
  • Rust: add methods to session.rs (see CLI-30 for unregister symmetry) and subcommands to mxgw-cli.
  • Tests: add per-client fixture/unit coverage for writeSecured (parity: fails without prior authenticate), authenticateUser, and adviseSupervisory; add these to the cross-language smoke matrix.
  • Docs: update each client README's capability list, the parity matrix in docs/ClientLibrariesDesign.md, and docs/CrossLanguageSmokeMatrix.md.

Verification. Per language, the CLAUDE.md per-area commands: .NET dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx + tests; Go go build/test ./...; Rust cargo check/test/clippy; Python python -m pytest; Java gradle test on windev (no local JRE on the Mac — revert the regenerated MxaccessGateway.java churn afterward, since no .proto changed).


CLI-05 — .NET session cannot be re-attached to an existing session id Medium ·

Finding. In the .NET client, MxGatewaySession's constructor is internal (clients/dotnet/ZB.MOM.WW.MxGateway.Client/MxGatewaySession.cs:19) and no public factory exists, unlike Go's NewSessionForID (clients/go/mxgateway/session.go:70), Java's forSessionId, Python's ctor, or Rust's client.session().

Impact. After a client restart, the gateway's DetachGraceSeconds/replay features are usable from .NET only through the raw stub — all typed helpers are lost on reconnect. Parity gap; .NET is the only client that cannot re-attach.

Design. Add a public factory MxGatewayClient.AttachSession(string sessionId) returning an MxGatewaySession whose OpenSessionReply is synthesized to carry the given SessionId (no server round-trip; the session already exists). Keep the internal ctor for the open path. This aligns .NET with the other four clients and unblocks CLI-15's reconnect helper. Alternative rejected: making the ctor public — the factory name documents intent (re-attach vs open) and keeps OpenSessionReply construction internal.

Implementation.

  • MxGatewayClient.cs: add public MxGatewaySession AttachSession(string sessionId) building a minimal OpenSessionReply { SessionId = sessionId } and calling the internal ctor.
  • Tests: add a case in the .NET session test project asserting AttachSession(id).SessionId == id and that a subsequent StreamEventsAsync(afterWorkerSequence) targets that id.
  • Docs: note re-attach in clients/dotnet/README.md and flip the parity-matrix cell in docs/ClientLibrariesDesign.md.

Verification. dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx and dotnet test the client test project.


CLI-06 — .NET DisposeAsync blocks/throws on unreachable gateway Medium ·

Finding. In the .NET client, MxGatewaySession.DisposeAsync calls CloseAsync() with no cancellation token or timeout (clients/dotnet/.../MxGatewaySession.cs:881-885). When the gateway is unreachable, await using disposal blocks for the full retry-pipeline budget and then throws out of disposal, masking the original exception.

Impact. await using on a session whose gateway just died hangs and then surfaces a disposal exception instead of the caller's real error — a poor failure mode on the common teardown path.

Design. Time-bound and swallow close failures in the disposal path only. Wrap the CloseAsync call in a short linked-timeout CTS and catch (OperationCanceledException/RpcException/MxGatewayException), since throwing from DisposeAsync is an anti-pattern that masks in-flight exceptions. Explicit CloseAsync() remains fully surfacing for callers who want to observe close failures. Keep the _closeLock.Dispose() unconditional.

Implementation.

  • MxGatewaySession.cs DisposeAsync: using var cts = new CancellationTokenSource(disposeCloseTimeout); (a small fixed bound, e.g. 2 s, or Options-derived), try { await CloseAsync(cts.Token); } catch (Exception ex) when (ex is OperationCanceledException or RpcException or MxGatewayException) { /* best-effort */ }.
  • Tests: add a test using a transport that never responds, asserting DisposeAsync completes within the bound and does not throw.
  • Docs: note best-effort dispose semantics in clients/dotnet/README.md.

Verification. dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx + client tests.


CLI-07 — .NET retry budget self-defeats on DeadlineExceeded Medium ·

Finding. In the .NET client, ExecuteSafeUnaryAsync caps the whole retry pipeline with CancelAfter(Options.DefaultCallTimeout) (clients/dotnet/.../MxGatewayClient.cs:305-316) while each attempt's CallOptions also gets a DefaultCallTimeout deadline (:290-303). Because DeadlineExceeded is in the retryable set (MxGatewayClientRetryPolicy.cs:62-67), a first attempt that ends in DeadlineExceeded consumes the entire outer budget, so the configured retries never run.

Impact. Retries are silently inert for the exact transient class they were configured for; Unavailable/ResourceExhausted bursts shorter than one full timeout still retry, but any deadline-driven transient does not.

Design. Give the outer budget headroom so retries have room to execute. Recommended: size the outer CTS to cover the retry math — roughly MaxAttempts × (DefaultCallTimeout + MaxDelay) — rather than a single DefaultCallTimeout. Alternative (simpler, less faithful to intent): drop DeadlineExceeded from IsTransientStatus. Prefer the headroom fix because per-attempt deadlines remain meaningful and deadline transients still retry. Open question the review couldn't settle: whether an absolute wall-clock cap should exist at all — recommend keeping one but computed from the retry parameters.

Implementation.

  • MxGatewayClient.cs ExecuteSafeUnaryAsync: compute the outer CancelAfter from Options.Retry (attempts, delay, max-delay) plus per-attempt DefaultCallTimeout, instead of a bare DefaultCallTimeout.
  • Tests: add a retry test whose first attempt returns DeadlineExceeded and asserts a second attempt occurs (the existing retry-policy test project already exercises the pipeline).
  • Docs: clarify the timeout/retry interaction in clients/dotnet/README.md.

Verification. dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx + retry test.


CLI-08 — .NET/Go/Java treat any nonzero HRESULT as failure Medium ·

Finding. EnsureMxAccessSuccess in .NET tests reply.HasHresult && reply.Hresult != 0 (clients/dotnet/.../MxCommandReplyExtensions.cs:32); Go tests reply.GetHresult() != 0 (clients/go/mxgateway/errors.go:121); Java's MxGatewayErrors does the same. This misclassifies positive COM success codes (e.g. S_FALSE = 1). Python uses the correct reply.hresult < 0 (clients/python/.../errors.py:133).

Impact. A parity-preserving gateway reply carrying a positive success HRESULT throws in three clients and passes in one — a cross-client inconsistency that can turn a legitimate MXAccess success into a raised error. Co-designed with CLI-03 (Rust must adopt < 0 from the start).

Design. Align .NET, Go, and Java on the COM-correct hresult < 0 check (negative = failure), matching Python and MXAccess semantics. Low-risk one-line change per client; keep the status-array check unchanged. Not individually enumerated in the roadmap, but shares the correctness theme of P0 item 6 (see CLI-03) — land alongside the Rust MXSTATUS work.

Implementation.

  • .NET MxCommandReplyExtensions.cs:32: bool hResultFailure = reply.HasHresult && reply.Hresult < 0;
  • Go errors.go:121: if reply.Hresult != nil && reply.GetHresult() < 0 {
  • Java MxGatewayErrors.java (~:50): change the != 0 comparison to < 0.
  • Tests: add a case in each client asserting a reply with hresult = 1 (S_FALSE) passes and hresult = -2147... fails; Python's existing regression suite already covers < 0.
  • Docs: note the corrected semantics in each client README's error section.

Verification. .NET build+test; Go go test ./...; Java gradle test on windev (revert generated-file churn afterward).


CLI-09 — Go has no typed auth-error mapping Medium ·

Finding. In the Go client, all RPC failures wrap into the generic GatewayError (clients/go/mxgateway/errors.go:9-34; call sites e.g. client.go), so distinguishing Unauthenticated from PermissionDenied requires status.Code(errors.Unwrap(err)). docs/ClientLibrariesDesign.md:153 requires the two be treated distinctly, and the other four clients expose typed auth errors.

Impact. Go consumers cannot idiomatically branch on auth failure vs authorization failure — the documented contract is unmet only in Go.

Design. Add sentinel-backed typed errors AuthenticationError (gRPC Unauthenticated) and AuthorizationError (PermissionDenied) in errors.go, and map them at the transport boundary where GatewayError is currently constructed. Use errors.Is-friendly sentinels (ErrUnauthenticated, ErrPermissionDenied) plus wrapper structs preserving Op and the underlying status.Status. This mirrors the existing MxAccessError/CommandError layering already in the file.

Implementation.

  • errors.go: add the two error types + sentinels and a helper classifyRPCError(op string, err error) error that inspects status.Code(err).
  • client.go (and other RPC wrappers): route transport errors through classifyRPCError instead of always constructing GatewayError.
  • Tests: add cases asserting errors.Is(err, ErrUnauthenticated) / ErrPermissionDenied for the corresponding gRPC codes.
  • Docs: update clients/go/README.md and flip the Go cell in the parity matrix (docs/ClientLibrariesDesign.md).

Verification. From clients/go: gofmt -l ., go build ./..., go test ./....


CLI-10 — Go uses deprecated grpc.DialContext + grpc.WithBlock() Medium ·

Finding. In the Go client, Dial builds options including grpc.WithBlock() and calls grpc.DialContext (clients/go/mxgateway/client.go:60-68). grpc-go ≥1.63 deprecates both in favor of grpc.NewClient with lazy connection.

Impact. Future grpc-go upgrades and go vet/staticcheck flag the deprecations; blocking dial also hides the per-RPC connection-error semantics the ecosystem now expects.

Design. Migrate to grpc.NewClient (lazy, non-blocking) and drop grpc.WithBlock(). Because NewClient no longer surfaces connect failures at dial time, expose readiness via a first Ping in the connect helpers that currently rely on blocking dial (or document that the first RPC surfaces connection errors). Preserve the existing transport-credential and auth-interceptor wiring unchanged.

Implementation.

  • client.go: replace grpc.DialContext(dialCtx, endpoint, dialOptions...) with grpc.NewClient(endpoint, dialOptions...), remove grpc.WithBlock(); if a caller-facing "connected" guarantee is desired, add an optional Ping in the Dial/connect wrapper.
  • Tests: existing dial/TLS tests should pass; add a case asserting a bad endpoint surfaces on first RPC rather than at construction.
  • Docs: note the lazy-connect behavior change in clients/go/README.md.

Verification. From clients/go: go build ./..., go vet ./..., go test ./....


CLI-11 — Go CLI cannot opt into strict TLS validation Medium ·

Finding. In the Go client CLI, dialForCommand never sets Options.RequireCertificateValidation and exposes no flag for it (clients/go/cmd/mxgw-go/main.go:1151-1158), so every non-CA-pinned TLS run of mxgw-go is skip-verify even though the library supports strictness (clients/go/mxgateway/client.go:231-241).

Impact. mxgw-go --tls without a CA cannot authenticate the server at all, with no operator opt-in — a security-posture gap unique to the Go CLI. Ties into CLI-17 (cross-client TLS convergence).

Design. Add a -require-certificate-validation boolean flag that sets Options.RequireCertificateValidation, mirroring the library capability. Coordinate the default with CLI-17's convergence decision; at minimum emit a one-line stderr warning when TLS runs skip-verify (see CLI-17/CLI-20).

Implementation.

  • clients/go/cmd/mxgw-go/main.go: register the flag and thread it into dialForCommand's Options.
  • Tests: extend the CLI/TLS tests to assert the flag sets the option.
  • Docs: document the flag in clients/go/README.md and the CLI usage.

Verification. From clients/go: go build ./..., go test ./....


CLI-12 — Java docs still say "Java 21" after the JDK 17 retarget Medium · P2

Finding. After the verified-correct JDK 17 retarget (build.gradle toolchain 17 + options.release = 17, build.gradle:20-31), three docs still claim Java 21: clients/java/README.md:354, clients/java/JavaClientDesign.md:34-35, and docs/ClientPackaging.md:193. This violates CLAUDE.md's docs-in-same-commit rule on the retarget's own branch.

Impact. Consumers targeting Ignition 8.3 (JDK 17) read contradictory prerequisites. Roadmap P2 item 15 (doc-drift sweep).

Design. Sweep all three references from "Java 21" to "Java 17" (noting a 17-targeted build still runs on 21+, as build.gradle:20-21 already comments). Pure documentation change; no code. Land on the feat/jdk17-client-retarget branch before merge.

Implementation.

  • clients/java/README.md:354, clients/java/JavaClientDesign.md:34-35, docs/ClientPackaging.md:193: change "Java 21" → "Java 17" (and correct the Gradle-toolchain wording). Note the docs/ClientPackaging.md:193 line also carries the stale Java package/naming addressed in CLI-16 — fix together.
  • Docs only; no tests.

Verification. Grep confirms zero remaining "Java 21" under clients/java and docs/: grep -rn "Java 21" clients/java docs/. No build needed for the doc change.


CLI-13 — Java event-stream buffer hardcoded 16, cancel-on-overflow Medium ·

Finding. In the Java client, MxGatewayClient.streamEvents constructs new MxEventStream(16) (clients/java/.../MxGatewayClient.java:248) with no configuration, and overflow cancels the RPC (MxEventStream.java:124-132). Unlike Go (CLI-01), the failure is at least surfaced as MxGatewayException("…queue overflowed"), but a consumer stalling for 16 events is disconnected even though gRPC has native flow control.

Impact. Slow Java consumers are force-disconnected at an arbitrary 16-event threshold; capacity is not tunable. Backpressure semantics diverge from the unbuffered .NET/Python/Rust clients (CLI-33).

Design. Make the buffer capacity an option on the stream-events call (default 16 for compatibility) and/or adopt manual flow control (disableAutoRequestWithInitial on the stub) so backpressure rides gRPC instead of cancel-on-overflow. Recommended minimal change: parameterize capacity via an overload / options object; the manual-flow-control migration is a larger follow-up. Keep the loud MxGatewayException on overflow (Java's surfaced-error behavior is already correct relative to Go).

Implementation.

  • MxGatewayClient.java: add an overload streamEvents(StreamEventsRequest, int bufferCapacity) (or an options field) feeding new MxEventStream(capacity).
  • MxEventStream.java: no behavior change required for the capacity option; document the overflow contract (see CLI-24).
  • Tests: extend the Java event-stream tests with a custom-capacity case and an overflow case asserting the MxGatewayException message.
  • Docs: record Java's buffered/error-on-overflow behavior in docs/ClientBehaviorFixtures.md (CLI-33) and clients/java/README.md.

Verification. gradle test from clients/java on windev (no local JRE); revert the regenerated MxaccessGateway.java churn afterward since no .proto changed.


CLI-14 — Python default TLS is blocking TOFU pin with silent localhost SNI Medium ·

Finding. In the Python client, create_channel's default TLS path fetches the server certificate unverified and pins it trust-on-first-use (clients/python/.../options.py:128-160), and silently defaults the SNI/target-name override to localhost when none is supplied (options.py:153-154). It is documented and bounded (probe timeout; asyncio.to_thread), but it is the only client that opens a second out-of-band TCP+TLS connection per channel, and TOFU is exposed to first-contact interception.

Impact. A first-contact MITM can pin a rogue certificate; the silent localhost SNI can mask a hostname mismatch. Bounded and intentional for the internal-tool posture, but the threat window is undocumented in the client's own README. Ties into CLI-17.

Design. Keep the TOFU default (removing it would break the internal-tool ergonomics the other clients also target) but document the MITM window in the README threat model, prefer ca_file in examples, and make the localhost SNI default explicit in a log/warning rather than silent. No behavioral break; a warning + doc change. Convergence across languages is CLI-17.

Implementation.

  • clients/python/README (or docs/): add a threat-model note describing the first-contact TOFU window and recommending ca_file; make ca_file the primary TLS example.
  • options.py: emit a one-line warning (via logging/warnings) when the TOFU path pins an unverified cert and when it auto-applies the localhost SNI override.
  • Tests: extend the Python TLS tests to assert the warning fires on the TOFU path.
  • Docs: cross-reference docs/CrossLanguageSmokeMatrix.md's TLS divergence table.

Verification. python -m pytest from clients/python.


CLI-15 — No client handles ReplayGap or offers a reconnect helper Medium · P2

Finding. All five clients expose the after_worker_sequence resume cursor (MxGatewaySession.cs:865-876, session.go:682-688, MxGatewaySession.java:718-722, session.py:571-582, session.rs:648), but none handle the ReplayGap sentinel a resuming consumer must expect — grep ReplayGap clients/ returns only build output (contracts XML, javadoc), no handwritten source. The gateway shipped DetachGraceSeconds + replay (on by default), so a resuming stream can legitimately emit a gap event that no client types or documents.

Impact. A client that reconnects with after_worker_sequence after the replay buffer overran receives a ReplayGap it does not recognize, silently mis-treating a lossy resume as continuous. Roadmap P2 item 12 (finish the session-resilience epic). Coordinates with CLI-04 (session-surface pass) and cross-domain with the gateway/testing reports.

Design. Two-stage: (1) document ReplayGap gap-detection per client README so consumers can branch on it via the raw event; (2) add a typed helper/observer that raises or signals gap detection during resume. Stage 1 is the P2-minimum. Recommended stage-2 shape: a per-client on_replay_gap/ReplayGapError hook surfaced from the event iterator when a ReplayGap event arrives, letting the consumer decide whether to re-snapshot. Do not synthesize or swallow the gap — forward the gateway's event faithfully (per the no-synthesized-events invariant); the client only makes it observable and typed.

Implementation.

  • Each client README: document that resuming with a stale after_worker_sequence can yield a ReplayGap event and what it means.
  • .NET/Go/Java/Python/Rust event surfaces: recognize the ReplayGap event kind and expose it as a distinct typed result/error alongside normal events.
  • Tests: add per-client fixture cases feeding a ReplayGap event and asserting the typed surface; add a resume-with-gap case to the cross-language smoke matrix.
  • Docs: docs/ClientLibrariesDesign.md (update the "no client-side reconnect" v1 non-goal, :64-70) and docs/CrossLanguageSmokeMatrix.md.

Verification. Per-language build/test as in CLI-04 (Java on windev, revert generated churn).


CLI-16 — docs/ClientPackaging.md drifted from Python naming and .NET .slnx Medium · P2

Finding. docs/ClientPackaging.md names the Python package mxaccess-gateway-client and its generated dir src/mxgateway/generated (:159-160) vs the actual zb-mom-ww-mxaccess-gateway-client (clients/python/pyproject.toml:8) and src/zb_mom_ww_mxgateway/generated; the CLI module python -m mxgateway_cli (:187) vs actual zb_mom_ww_mxgateway_cli; the .NET solution ZB.MOM.WW.MxGateway.Client.sln (:51-52) vs actual .slnx; and Java 21 (:193, CLI-12). docs/ClientLibrariesDesign.md:410 repeats the stale Python generated path.

Impact. Copy-paste build/run commands from the packaging doc fail — wrong package name, wrong module path, wrong solution extension. Roadmap P2 item 15.

Design. One documentation sweep aligning every reference to reality. Pure docs; no code. Land with CLI-12 (same file, :193).

Implementation.

  • docs/ClientPackaging.md: :51-52 .sln.slnx; :159-160 package → zb-mom-ww-mxaccess-gateway-client, generated dir → src/zb_mom_ww_mxgateway/generated; :187 CLI module → python -m zb_mom_ww_mxgateway_cli; :193 Java 21 → 17 and package naming.
  • docs/ClientLibrariesDesign.md:410: generated path → clients/python/src/zb_mom_ww_mxgateway/generated.
  • Verify against clients/python/pyproject.toml and clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx.

Verification. Grep the corrected tokens back against the source: grep -n "slnx\|zb_mom_ww_mxgateway" docs/ClientPackaging.md and confirm no stale mxgateway_cli/src/mxgateway/generated remain.


CLI-17 — TLS default posture inconsistent across the five clients Medium ·

Finding. The five clients diverge on default TLS: accept-any-cert in .NET (MxGatewayClient.cs:361-364), Go (client.go:236-240, InsecureSkipVerify), and Java (InsecureTrustManagerFactory); TOFU pinning in Python (options.py:133-154); strict pin-only in Rust. docs/CrossLanguageSmokeMatrix.md:58-66 documents the divergence, but the same --tls-without-CA invocation authenticates the server in one language, half in another, and not at all in three.

Impact. Inconsistent, surprising security posture per language for identical invocations. Depends on CLI-14 (Python) and interacts with CLI-11/CLI-20.

Design. Converge, or at minimum make the weak default loud. Recommended target: the Python TOFU model as the shared default (pins on first contact, better than accept-all), and where a client keeps accept-all as an explicit opt-in, emit a one-line stderr/log warning whenever certificate verification is disabled. Full convergence is a larger cross-client effort; the review's minimum is the warning. Open question the review left: whether to make strict validation the default and require an explicit --insecure-style opt-out — recommended long-term but a breaking change for existing internal-tool callers, so stage it behind a major version.

Implementation.

  • .NET MxGatewayClient.cs:361-364, Go client.go, Java TLS path: emit a single warning when the accept-all callback / InsecureSkipVerify / InsecureTrustManagerFactory is installed (see CLI-20 for the .NET-specific note).
  • Optionally add TOFU pinning to .NET/Go/Java to match Python (larger; stage separately).
  • Tests: assert the warning fires when TLS runs without a CA and without strict validation.
  • Docs: reconcile docs/CrossLanguageSmokeMatrix.md:58-66 with the chosen convergence and note the warning behavior in each README.

Verification. Per-language build/test (Java on windev).


CLI-18 — .NET csproj records no <Version> Low · P2

Finding. clients/dotnet/.../ZB.MOM.WW.MxGateway.Client.csproj (packaging block lines 19-28) declares no <Version>; the published NuGet version (0.1.2) is supplied out-of-band at pack time, so the source tree does not record what ships.

Impact. The repo cannot be inspected to learn the shipped version; drift risk. Roadmap P2 item 16 (version alignment).

Design. Add <Version> to the csproj PropertyGroup so the source records the shipped version, keeping the pack-time override available for CI. Trivial.

Implementation.

  • ZB.MOM.WW.MxGateway.Client.csproj: add <Version>0.1.2</Version> (or the current release) to the packaging PropertyGroup (lines 19-28).
  • Docs: none beyond the version-alignment note in the packaging doc.

Verification. dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx.


CLI-19 — .NET duplicate InternalsVisibleTo Low ·

Finding. InternalsVisibleTo("ZB.MOM.WW.MxGateway.Client.Tests") is declared both in clients/dotnet/.../Properties/AssemblyInfo.cs:3 and as a csproj AssemblyAttribute (ZB.MOM.WW.MxGateway.Client.csproj:35-39). Harmless but redundant.

Impact. Cosmetic; two sources of truth for the same attribute.

Design. Keep one. Recommend the csproj AssemblyAttribute block (co-located with packaging config) and delete Properties/AssemblyInfo.cs, or vice versa.

Implementation.

  • Delete clients/dotnet/.../Properties/AssemblyInfo.cs (or remove the csproj block) — keep exactly one declaration.

Verification. dotnet build clients/dotnet/ZB.MOM.WW.MxGateway.Client.slnx (confirms tests still see internals).


CLI-20 — .NET --tls without CA installs accept-all callback Low ·

Finding. In the .NET client, with UseTls and no CA file, the default RequireCertificateValidation = false installs an accept-all certificate callback (clients/dotnet/.../MxGatewayClient.cs:361-364). Documented and intentional (MxGatewayClientOptions.cs:31-36), but --tls without a CA gives no server authentication.

Impact. Same class as CLI-17; called out separately for the .NET default. Low because documented/intentional.

Design. Emit a one-line warning when the accept-all callback is installed (co-designed with CLI-17). No behavior change to the default.

Implementation.

  • MxGatewayClient.cs: log a warning (via the client's ILogger) when the accept-all RemoteCertificateValidationCallback is installed (line 361-364 branch).
  • Docs: cross-reference CLI-17 in clients/dotnet/README.md.

Verification. dotnet build + client TLS-handler test asserting the warning.


CLI-21 — Go ClientVersion stale vs tagged releases Low · P2

Finding. In the Go client, ClientVersion = "0.1.0-dev" (clients/go/mxgateway/version.go:6) is stale relative to the module releases published via scripts/tag-go-module.ps1.

Impact. mxgw-go and callers report a dev version regardless of the tagged release. Roadmap P2 item 16.

Design. Bump ClientVersion to the released value and update it as part of the tagging script so the two cannot drift.

Implementation.

  • clients/go/mxgateway/version.go:6: set to the current release (e.g. 0.1.2).
  • scripts/tag-go-module.ps1: update the constant (or fail the tag) when the version constant does not match the tag.
  • Docs: version-alignment note in the packaging doc.

Verification. From clients/go: go build ./..., go test ./....


CLI-22 — Go newCorrelationID swallows crypto/rand error Low ·

Finding. In the Go client, newCorrelationID returns an empty string on rand.Read error (clients/go/mxgateway/session.go:786-792), silently dropping traceability.

Impact. A rare crypto/rand failure yields empty correlation ids, breaking request tracing with no signal.

Design. Fall back to a monotonic timestamp+counter id rather than empty, preserving uniqueness/traceability without introducing an error-return on a hot helper.

Implementation.

  • session.go:786-792: on rand.Read error, build a fallback id from time.Now().UnixNano() plus an atomic counter.
  • Tests: add a case (via an injectable rand source or by asserting non-empty output) that the id is never empty.

Verification. From clients/go: go test ./....


CLI-23 — Go nil-vs-empty bulk short-circuit asymmetry Low ·

Finding. In the Go client, WriteBulk/ReadBulk short-circuit an empty (non-nil) slice locally (session.go:399-407, :524-532) while AddItemBulk/SubscribeBulk send an empty command to the wire (session.go:255-274). Harmless but inconsistent within one file.

Impact. Cosmetic; identical inputs produce a local no-op in some helpers and a wire round-trip in others.

Design. Pick one convention (recommend short-circuit empty locally, avoiding a pointless round-trip) and apply it uniformly across the bulk helpers.

Implementation.

  • session.go: make AddItemBulk/SubscribeBulk (and siblings) short-circuit empty input like WriteBulk/ReadBulk, or document the intentional difference.
  • Tests: assert empty-input behavior is consistent.

Verification. From clients/go: go test ./....


CLI-24 — Java MxEventStream single-consumer constraint undocumented Low ·

Finding. In the Java client, MxEventStream is a single-consumer iterator with unsynchronized next state (MxEventStream.java:31, :65-92); the single-consumer constraint is not documented.

Impact. A caller iterating from two threads corrupts next state with no warning.

Design. Add a Javadoc note stating the iterator is single-consumer / not thread-safe. Documentation only (matching CLI-13's overflow doc).

Implementation.

  • MxEventStream.java: Javadoc on the class and next noting single-consumer usage.

Verification. gradle test on windev (revert generated churn); Javadoc-only change.


CLI-25 — Java close() does not await channel termination Low ·

Finding. In the Java client, close() initiates shutdown() without awaiting termination (MxGatewayClient.java:346-351); closeAndAwaitTermination() exists (:360-367), but try-with-resources users can leak a channel briefly at JVM exit.

Impact. Minor resource leak window on try (var client = ...) teardown.

Design. Make close() await a short bounded termination (e.g. a few seconds) so try-with-resources users get clean teardown, keeping closeAndAwaitTermination() for explicit longer bounds.

Implementation.

  • MxGatewayClient.java close(): after shutdown(), awaitTermination(shortBound, SECONDS) (swallow/timeout gracefully).
  • Tests: assert close() completes and the channel is terminated.
  • Docs: note the bounded-await in clients/java/README.md.

Verification. gradle test on windev; revert generated churn.


CLI-26 — Python version.pypyproject.toml Low · P2

Finding. In the Python client, pyproject.toml:9 says 0.1.2 while version.py:3 says __version__ = "0.1.0", so mxgw-py version reports the wrong value.

Impact. CLI and callers report a stale version. Roadmap P2 item 16.

Design. Derive __version__ from installed metadata so the two cannot drift: importlib.metadata.version("zb-mom-ww-mxaccess-gateway-client") with a fallback for editable/source runs. Aligns with the single-source-of-truth intent.

Implementation.

  • clients/python/src/zb_mom_ww_mxgateway/version.py: replace the literal with importlib.metadata.version(...) guarded by try/except PackageNotFoundError falling back to a literal.
  • Tests: assert version.__version__ matches pyproject (or the installed dist) in the packaging/regression suite.
  • Note: any Python regen for this work must pin grpcio-tools to the baseline (grpcio 1.80.0 / protobuf 6.31.1) — but this change touches no protos, so no regen is needed.

Verification. python -m pytest from clients/python.


CLI-27 — Python Session.close() not concurrency-safe Low ·

Finding. In the Python client, Session.close() has no lock around _closed (session.py:38-55) and repeated closes return a locally synthesized CloseSessionReply rather than the cached server reply — divergent from .NET/Go which cache the real reply.

Impact. Concurrent closes can double-invoke; repeated closes return a synthetic reply instead of the server's. Minor.

Design. Add an asyncio.Lock around the close transition and cache the real server CloseSessionReply to return on repeat, matching the .NET/Go cached-reply pattern.

Implementation.

  • session.py: guard _closed with an asyncio.Lock, store the first server reply, return it on subsequent calls.
  • Tests: assert concurrent close() calls invoke the server once and return the cached reply.

Verification. python -m pytest from clients/python.


CLI-28 — Python circular-import workaround at end of session.py Low ·

Finding. In the Python client, from .client import GatewayClient sits at the bottom of session.py with # noqa: E402 (~session.py:590) to break a runtime import cycle.

Impact. Works, but the runtime cycle is fragile and lints noisily.

Design. Replace with a TYPE_CHECKING-guarded import plus a string annotation, removing the runtime cycle entirely.

Implementation.

  • session.py: if TYPE_CHECKING: from .client import GatewayClient at the top and annotate the reference as "GatewayClient"; delete the bottom-of-file import and # noqa.
  • Tests: existing import/session tests confirm no runtime cycle.

Verification. python -m pytest from clients/python.


CLI-29 — Rust CLIENT_VERSIONCargo.toml Low · P2

Finding. In the Rust client, CLIENT_VERSION = "0.1.0-dev" with a doc comment claiming it "Mirrors Cargo.toml" (clients/rust/src/version.rs:6-7) while Cargo.toml:3 says 0.1.2.

Impact. The advertised client version is wrong and contradicts its own doc. Roadmap P2 item 16.

Design. Source the constant from Cargo at compile time so it cannot drift: env!("CARGO_PKG_VERSION").

Implementation.

  • clients/rust/src/version.rs:7: pub const CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION");.
  • Tests: add a trivial assertion that CLIENT_VERSION equals the expected release, or that it is non-empty and matches Cargo.toml via a build script check.

Verification. From clients/rust: cargo check --workspace, cargo test --workspace, cargo clippy --all-targets -- -D warnings.


CLI-30 — Rust has no unregister typed helper Low ·

Finding. In the Rust client, the session surface (clients/rust/src/session.rs:119-231) covers register/add/advise/remove but not Unregister; Go/Java/Python expose it (and .NET also lacks it per the parity matrix).

Impact. Rust (and .NET) users must drop to raw Invoke to unregister — a small parity gap.

Design. Add an unregister typed helper wrapping the existing raw-command machinery, for symmetry with the other clients. Fold into the CLI-04 session-surface pass.

Implementation.

  • clients/rust/src/session.rs: add pub async fn unregister(...) mirroring register.
  • (Optionally add the .NET UnregisterAsync too, closing the matrix cell.)
  • Tests: fixture/unit coverage for unregister.
  • Docs: flip the parity-matrix cell in docs/ClientLibrariesDesign.md.

Verification. From clients/rust: cargo check/test/clippy.


CLI-31 — Rust CLI is a single 2,699-line main.rs Low ·

Finding. In the Rust client, the CLI is one 2,699-line main.rs (clients/rust/crates/mxgw-cli/src/main.rs), the largest single file in the client tree; the Windows stack-size workaround it forced (clients/rust/.cargo/config.toml:1-19) is evidence the command enum has outgrown one module.

Impact. Maintainability; large compile unit; the stack-size workaround is a symptom.

Design. Split subcommands into per-command modules (mxgw-cli/src/commands/*.rs) with main.rs reduced to arg parsing and dispatch. Mechanical refactor; behavior-preserving. Re-evaluate whether the .cargo/config.toml stack workaround can be relaxed after the split.

Implementation.

  • clients/rust/crates/mxgw-cli/src/: extract subcommand handlers into modules; keep the clap/enum wiring in main.rs.
  • Tests: existing CLI tests must pass unchanged.

Verification. From clients/rust: cargo build, cargo test --workspace, cargo clippy --all-targets -- -D warnings.


CLI-32 — Client-side bulk caps differ Low ·

Finding. Go/Python/Rust enforce a 1,000-item client-side bulk cap (session.go:19, session.py:11, session.rs:29) while .NET and Java send unbounded lists and rely on the gateway. Same oversized call produces different error types per language.

Impact. Inconsistent error surface for oversized bulk calls across languages. Harmless functionally.

Design. Align: either all clients enforce the 1,000 cap locally (recommended — a fast, uniform client-side error) or none do. Add the cap to .NET and Java.

Implementation.

  • .NET (MxGatewaySession.cs) and Java (MxGatewaySession.java): add a client-side 1,000-item check on the bulk helpers, throwing the client's standard argument/validation error.
  • Tests: assert an oversized bulk call fails locally with the expected error type in .NET and Java.
  • Docs: note the uniform cap in the client READMEs.

Verification. .NET build+test; Java gradle test on windev (revert generated churn).


CLI-33 — Per-language event backpressure semantics undocumented Low ·

Finding. Event-stream backpressure differs by language: Go buffered-16 / silent-cancel (CLI-01), Java buffered-16 / error-cancel (CLI-13), .NET/Python/Rust unbuffered (native gRPC flow control). This per-language slow-consumer behavior is a parity-relevant observable absent from docs/ClientBehaviorFixtures.md.

Impact. Consumers cannot predict slow-consumer behavior from the docs. Depends on CLI-01 and CLI-13 landing first (they change the Go/Java behavior being documented).

Design. After CLI-01 (Go now emits a terminal ErrSlowConsumer) and CLI-13 (Java capacity option), document the per-language slow-consumer contract in docs/ClientBehaviorFixtures.md as a parity fixture row. Documentation only.

Implementation.

  • docs/ClientBehaviorFixtures.md: add a backpressure/slow-consumer section stating each client's behavior (Go: terminal ErrSlowConsumer; Java: MxGatewayException on overflow, configurable capacity; .NET/Python/Rust: native gRPC flow control).
  • Optionally add a shared behavior-fixture case exercised by each client's fixture-driven tests.

Verification. Doc change; validated by the fixture-driven test suites already run per client.


CLI-34 — Python build//.pytest_cache/ present on disk (untracked) Low ·

Finding. In the Python client, build/ and .pytest_cache/ exist on disk but are not git-tracked (report X7; generated-code hygiene is otherwise good across all clients — tracked generated dirs match the manifest, Rust uses OUT_DIR + .gitkeep, .NET references Contracts directly).

Impact. None functionally; risk of an accidental future commit of build artifacts.

Design. Confirm clients/python/.gitignore (or the repo root .gitignore) covers build/ and .pytest_cache/; add entries if absent. No code change.

Implementation.

  • Verify/extend .gitignore to cover clients/python/build/ and clients/python/.pytest_cache/.

Verification. git status --ignored clients/python shows the directories ignored, not untracked.