Completes both findings across all five clients — done locally on the Mac now
that homebrew openjdk@17 is available (JAVA_HOME=/opt/homebrew/opt/openjdk@17).
CLI-15: new MxEventStreamItem record + MxEventStream.nextItem() surfaces the
gateway's ReplayGap sentinel as a typed, non-terminal signal (isReplayGap()/
replayGap()/event()); existing Iterator<MxEvent> path unchanged, sentinel never
swallowed/synthesized. Javadoc covers gap semantics + resume contract.
CLI-04: typed single-item helpers on MxGatewaySession — Phase 1
adviseSupervisory/writeSecured/writeSecured2/authenticateUser/archestrAUserToId,
Phase 2 addBufferedItem/setBufferedUpdateInterval/suspend/activate (unregister
already present). Each routes through invokeCommand -> ensureProtocolSuccess +
ensureMxAccessSuccess (same validation as bulk). MXAccess parity preserved.
Credentials flow only into the request proto; exceptions carry only the reply and
gRPC status text is scrubbed via MxGatewaySecrets.redactCredentials — tests
assert the password/secured value is absent from getMessage()/toString()/CLI
output. New CLI subcommands write-secured/authenticate-user (credential via
--password/--password-env, prints only the user id).
gradle test: 106 tests, 0 failures (58 client + 48 cli); no generated churn.
Shared docs: ClientLibrariesDesign + CLAUDE.md updated to "all five clients".
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Java client toolchain (homebrew openjdk@17) works on the Mac, so the remaining
Java halves of CLI-15/CLI-04 are done locally this session, not batched to windev.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Resolve the session-resilience epic's shipped-vs-planned entanglement:
- Phase 3 (reconnect) finished: Task 13 = TST-02 (owner-scoped attach, P0),
Task 15 = TST-01 (reconnect integration test), Task 14 = CLI-15 for 4/5
clients (Java pending, windev batch).
- Phase 4 (per-session dashboard ACL) scoped as TST-15; the open Viewer-default
decision is settled: admin-sees-all, Viewer strict per owned/granted session
(matches TST-02's gRPC owner binding).
- Phase 5 (orphan-worker reattach) marked DEFERRED, not planned. The
EnableOrphanReattach flag does not exist and must not be referenced as if it
does. The CLAUDE.md "gateway restart does not reattach orphan workers"
invariant stands.
Updates oldtasks.md, the tasks.json mirror (statuses + governance note), and the
CLAUDE.md reconnect paragraph (clients now consume ReplayGap; reattach deferred).
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Proves the default-on reconnect protocol end to end through the real gRPC
StreamEvents path via the fake worker harness (no live COM). Two facts:
- ReconnectInsideRetainedWindow_ReplaysTailNoGap: capacity 16 retains all
events; reconnect from a mid-batch after_worker_sequence cursor replays
exactly the events with WorkerSequence > cursor (retained tail + events
emitted while detached), strictly ascending, distinct, no ReplayGap.
- ReconnectWithStaleCursor_EmitsReplayGapSentinelFirst: capacity 3 with 6
events forces eviction; reconnect with AfterWorkerSequence=1 yields the
ReplayGap sentinel first (Family=Unspecified, no body, RequestedAfterSequence=1,
OldestAvailableSequence=oldest retained), then the retained tail ascending.
Single-subscriber mode: the first stream is fully detached (cancel + await the
stream task runs EventStreamService's finally, dropping the subscriber count to
0) before reconnect; the distributor + replay ring are created once per session
and survive detach, so events emitted while detached are retained.
macOS note: fake-worker E2E tests need TMPDIR=/tmp (default macOS TMPDIR pushes
the CoreFxPipe Unix-socket path past the 104-byte sun_path limit). Windows CI
is unaffected. Server reconnect/replay behavior matched the contract exactly.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
The gateway emits a ReplayGap sentinel MxEvent at the head of a StreamEvents
stream resumed via after_worker_sequence when the requested cursor predates the
oldest retained event. Clients previously ignored it, silently mis-treating a
lossy resume as continuous. Each client now surfaces the sentinel as a distinct,
typed, non-terminal signal (never synthesized, never swallowed) so a consumer can
detect the gap and re-snapshot; resume contract is
after_worker_sequence = oldest_available_sequence - 1.
- .NET: MxEventStreamItem (IsReplayGap/ReplayGap/Event) via new StreamEventItemsAsync
+ AsStreamItemsAsync extension. Build clean, 87 passed.
- Go: EventResult.ReplayGap field + IsReplayGap(); ReplayGap type alias. build/vet/test clean.
- Rust: EventItem enum (Event/ReplayGap); EventStream now yields Result<EventItem, Error>;
CLI renders REPLAY_GAP line / replayGap JSON. fmt/check/test/clippy clean.
- Python: ReplayGap dataclass; stream_events yields pb.MxEvent | ReplayGap. 131 passed.
- Shared docs: ClientLibrariesDesign non-goals reframed (reconnect-replay protocol is
consumable; auto-reconnect stays a non-goal); CrossLanguageSmokeMatrix resume-gap note.
Java client is deferred to the windev batch (no local JRE); CLI-15 stays open until it lands.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
WRK-06: MxStatusProxyConverter caches the four resolved FieldInfo per status
type in a static ConcurrentDictionary (the GetField metadata scan ran 4x per
status per event on the STA path). GetValue+Convert.ToInt32 still run per event
(late-bound RCW). Exceptions byte-identical: missing-field message unchanged
(ResolveField, not cached on throw via GetOrAdd); null-value message unchanged.
WRK-11: MxAccessEventQueue.Enqueue takes ownership of the passed MxEvent -
stamps WorkerSequence/WorkerTimestamp on it in place and enqueues it, no
Clone(). Audited all 3 callers (base/alarm event sinks, provider-mode handler):
each builds a fresh event per Enqueue, none reuse it. MxAccessValueCache.Set now
deep-copies its retained Value/SourceTimestamp/Statuses so the cache snapshot
never aliases the queue-owned (later serialized) event. Net: alarm/other events
clone nothing (was full clone); data-change clones payload-only.
WRK-12: WorkerFrameWriter coalesces the flush across a drained batch - each
frame is written but not flushed individually; one FlushAsync after the batch,
then all written frames complete. Preserves the written+flushed completion
contract; a burst of N events costs 1 flush, not N. On write failure the whole
in-flight batch + queue fail so no caller hangs.
IPC-15 (doc): the multi-event WorkerEnvelope body remains unimplemented (wire
still carries one event per worker_event frame); gateway.md Performance section
now distinguishes the shipped flush-coalescing from that deferred proto change.
net48-safe (no init/records; readonly struct cache entry). Worker builds x86
only - verification on windev. Tests added: converter cache-reuse, queue
ownership-transfer, value-cache snapshot independence, writer batch-flush-once.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
TST-11: single-source the .NET-side version in src/Directory.Build.props so
Server/Worker/Contracts/tests stamp 0.1.2 (was the SDK default 1.0.0) and
InformationalVersion carries the git short SHA (0.1.2+<sha>) for support
correlation; the git query is guarded (ContinueOnError) so a build outside a
git checkout still succeeds. Verified: Server assembly stamps 0.1.2+579282f,
build clean. Kept at 0.1.2 (matches Contracts + aligned Python/Rust/Go
clients); Java leads at 0.2.0 post JDK-17 retarget. Convergence to a single
0.2.0 cadence left as a release decision, not forced here.
WRK-15 (docs-only, no x86 worker build needed): correct the STA thread name in
docs/WorkerSta.md + docs/MxAccessWorkerInstanceDesign.md to the actual
MxGateway.Worker.STA (was the FQ ZB.MOM.WW.MxGateway.Worker.STA), and fix the
stale heartbeat-counter note - CaptureHeartbeat now populates event queue depth
(eventQueue.Count) and sequence (LastEventSequence) from the live queue.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
SEC-25 (near-term hardening; full per-session EventsHub ACL stays deferred to
roadmap item 12): DashboardEventBroadcaster now redacts tag values from a deep
CLONE of the event before mirroring to SignalR when Dashboard:ShowTagValues is
false (the default). Clears MxEvent.value (covers OnDataChange/OnWriteComplete/
OperationComplete/OnBufferedDataChange — their bodies are empty discriminators,
values ride in the top-level field incl. buffered arrays) and the alarm body's
current_value/limit_value. Source event never mutated (shared with the gRPC
path + replay ring) - verified by a source-not-mutated test. Makes the formerly
dead ShowTagValues flag live for the mirror. EventsHub TODO(per-session-acl)
kept and tied to roadmap item 12. Tests: DashboardEventBroadcasterTests (3).
SEC-30: trim docs/Diagnostics.md to mark opt-in command-value logging as NOT
YET IMPLEMENTED (no LogCommandValues knob, RedactCommandValue has no call
sites, no values logged); wiring deferred pending secured-bulk redaction
coverage (SEC-13). No option/call sites added.
Server build clean (0 warnings); Dashboard tests 152/152.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Replace the replay buffer's LinkedList<ReplayEntry> (a node allocation per
retained event on the fan-out hot path) with a preallocated ReplayEntry[] ring
sized to ReplayBufferCapacity, tracked by _replayHead (oldest) + _replayCount.
Appending a retained event now allocates nothing.
Behavior preserved exactly: ascending-WorkerSequence append order; capacity
eviction (overwrite head + advance when full); time trim via
_timeProvider.GetUtcNow() cutoff at the same three call sites; oldest-read for
ReplayGap math; both replay-from-sequence query paths + highest-seen tracking;
same _replayLock. Capacity-0 stays retain-nothing (guarded early return, no
modulo-by-zero).
Server build clean (0 warnings); Distributor/Replay tests 33/33 incl. two new
cases (multi-wrap ring keeps newest in order; capacity-1 overwrite + gap).
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Behavior-preserving allocation cuts on the per-event/per-command path:
- GWC-06: StreamEvents send-timing uses Stopwatch.GetTimestamp() +
GetElapsedTime() instead of a per-event Stopwatch allocation (same measured
span).
- GWC-07/IPC-05 (event): MapEvent transfers ownership of the inner MxEvent
instead of .Clone()-ing it. Safe: WorkerEvent is parsed fresh per pipe frame
with the distributor pump as its single consumer (GWC-01), MapEvent runs once
before fan-out, and every downstream consumer (subscribers, replay ring)
only READS the event (WorkerSequence is stamped upstream; verified no
post-mapping mutation). Comment documents the invariant + restore-clone
caveat if a second consumer is added.
- IPC-05 (command): CreateCommandEnvelope no longer re-clones; MapCommand
already isolated the graph from the caller-owned gRPC message.
- GWC-15: grpc_stream_queue.depth converts from a per-event push counter to an
ObservableGauge summing registered channel sources at scrape time only
(name/semantics unchanged); removes all per-event .Count/lock work.
Kept every load-bearing isolation clone (MapCommand, Invoke, bulk filters,
MapCommandReply). Server build clean (0 warnings); EventStream/Metrics/
Distributor/Mapper tests 62/62 (incl. formerly-flaky queue-depth tests, now
green under the lazy gauge, + 2 new MapEvent ownership tests). Docs: Metrics.md,
Grpc.md.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Gateway server-side named-pipe frame path, wire format byte-identical:
- Writer (GWC-08/IPC-13): serialize once into a single ArrayPool-rented buffer
holding the 4-byte LE length prefix + payload, then one WriteAsync. Removes
the second serialization pass (ToByteArray re-ran CalculateSize), the
separate prefix array + second stream write, and per-frame heap allocation.
- Reader (IPC-14): rent the payload buffer from ArrayPool instead of new byte[]
per frame; read/parse bounded to the real length, return in finally. Matches
the worker side which already pools.
- Correctness: length tracked separately from (larger) pool capacity; every
rented buffer returned exactly once in finally incl. the malformed-protobuf
path; ParseFrom copies into the message so the envelope never aliases the
returned buffer.
Cross-checked LE-prefix framing agrees across gateway+worker writer/reader.
Server build clean (0 warnings); WorkerFrameProtocol tests 10/10 incl. a new
large-payload-near-cap round-trip (forces pool buffer > frame). The 2 failing
WorkerClient/FakeWorkerHarness tests are the known macOS UDS 104-char path
limit, not a regression.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Restore the __Host- browser guarantees for the default secure deployment
without breaking plaintext/dev:
- DashboardServiceCollectionExtensions PostConfigure now resolves the cookie
name as: explicit MxGateway:Dashboard:CookieName override wins; else
__Host-MxGatewayDashboard when SecurePolicy==Always (RequireHttpsCookie
true); else the plain MxGatewayDashboard default. Guard: never apply the
__Host- prefix without Secure (browsers silently drop it).
- DashboardAuthenticationDefaults: add SecureCookieName const; keep the plain
CookieName as the non-secure fallback.
- Docs corrected to the actual conditional contract (five stale claims):
gateway.md, GatewayProcessDesign.md, ImplementationPlanGateway.md,
GatewayDashboardDesign.md, CLAUDE.md; GatewayConfiguration.md phrasing
tightened.
- Test: DashboardCookieOptionsTests asserts the name flips with
RequireHttpsCookie and that an explicit override wins.
Server build clean (0 warnings); Dashboard tests 149/149.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Reconcile load-bearing docs with shipped behavior:
- IPC-06: gateway.md Worker Envelope sketch -> points to mxaccess_worker.proto
as source of truth (string correlation_id, real oneof arms incl.
worker_shutdown_ack/worker_ready).
- IPC-07: docs/Grpc.md six RPCs -> seven; document QueryActiveAlarms handler
+ validation row.
- IPC-21: gateway.md Session RPC moved from live API into a 'Future work: not
implemented' subsection.
- TST-13: drop stale design-era sketches from gateway.md; correct the
single-subscriber-default (config-gated fan-out) note.
- SEC-09: dashboard GroupToRole sample GwAdmin:Admin -> Administrator so it
passes GatewayOptionsValidator; clarify Administrator is the canonical role.
- SEC-22: rewrite docs/Authentication.md to the pipeline that actually ships
(ZB.MOM.WW.Auth.ApiKeys package + gateway-owned CachingApiKeyVerifier,
CoalescingMarkApiKeyStore, CanonicalForwardingApiKeyAuditStore, etc.);
remove 18 stale type names (grep-verified absent).
- IPC-17: correct wrong Python generated dir (mxgateway -> zb_mom_ww_mxgateway)
in CLAUDE.md + 3 docs.
- CLI-12: Java docs Java 21 -> Java 17 (JDK17 retarget for Ignition 8.3).
- CLI-16: docs/ClientPackaging.md reconciled with real .slnx, Python package
name, and gradle project names; fix stale generateProto task name.
Docs-only; type/path/version claims verified against source.
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
Gateway-side follow-up to the shared auth-lib expiry core (delivered via G-2):
- apikey create-key gains optional --expires — absolute ISO-8601 UTC or relative
<N>d/<N>h from now; omitted means non-expiring (opt-in, unchanged default).
Threaded ApiKeyAdminCommand -> parser -> runner into the library's
CreateKeyAsync(..., expiresUtc, ...). list-keys shows an expiry column and an
active/expired/revoked status.
- Dashboard API Keys page surfaces expiry: an Expires column (Never when unset)
and a status badge reading Expired (red) / Expiring (<=7d, amber) / Revoked /
Active. DashboardApiKeySummary.ExpiresUtc projected in DashboardSnapshotService;
StatusBadge maps the new states.
Tests: parser (absolute/relative/invalid/none) + end-to-end past-expiry rejection
through the live verifier; dashboard summary suite green. Docs: Authentication.md
(verification-flow expiry step, CLI table + examples, dashboard badge).
Closes the tracked SEC-10 polish (SEC-10 core was already Done).
The read loop awaited EnqueueWorkerEventAsync inline, which blocks in the bounded
event channel's timed WriteAsync (up to EventChannelFullModeTimeout, default 5s)
when the channel is full with no/slow consumer. A WorkerCommandReply or heartbeat
queued behind an event frame was then stalled, so an in-flight InvokeAsync could
hit CommandTimeout even though the worker replied in time.
Mirror the existing outbound WriteLoopAsync: add an unbounded event staging
channel and a dedicated EventWriteLoopAsync. DispatchEnvelope is now fully
synchronous — the WorkerEvent branch hands the event off with a non-blocking
TryWrite and the read loop never awaits. The event write loop owns the timed
WriteAsync into the bounded channel and the sustained-overflow ProtocolViolation
fault (unchanged contract). Registered in WaitForBackgroundTasks + completed on
close/fault/dispose.
Test: reply arriving after events with a full, consumer-less event channel is
dispatched promptly (no CommandTimeout) — pipe-harness, verified on windev.
Docs: GatewayProcessDesign read/write/event-loop section.
Follow-up to the IPC-02 proto change and IPC-03 headroom default:
- regenerate clients/proto/descriptors/mxaccessgw-client-v1.protoset with the
pinned protoc 34.1 so it carries GatewayHello.max_frame_bytes (IPC-01 descriptor
refresh; ClientProtoInputTests.Descriptor_ContainsEveryContractMessageAndField).
- update GatewayOptionsTests design-default assertion to 16 MiB + 64 KiB reserve.
Worker half of the Wave 3 size/backpressure + write-ordering pass:
- IPC-02: the worker adopts GatewayHello.max_frame_bytes during the handshake
(WorkerFrameProtocolOptions.AdoptNegotiatedMaxMessageBytes) instead of a
hard-coded default; 0 keeps the default, a value above a 256 MiB ceiling is
rejected. Reader and writer share the options instance, applied before the
message loop.
- IPC-04: DrainEvents caps each reply at MaxDrainEventsPerReply (10_000) and
treats max_events = 0 as that cap rather than 'drain the entire queue', so one
diagnostic drain cannot pack a session-killing reply frame.
- WRK-04: WorkerFrameWriter stamps the envelope Sequence at the actual point of
writing (under the write lock) instead of at envelope creation, so the on-wire
order and the stamped sequence always agree under concurrent producers.
- WRK-07: the writer is now a cooperative priority scheduler — callers enqueue at
Control or Event priority and the draining lock-holder writes all control
frames before any event frame, so replies/faults/heartbeats jump ahead of an
event backlog. Per-frame validation/size rejections fail only that frame; a
stream write failure fails all queued frames.
Tests: monotonic gap-free sequence under concurrency, control-before-event
priority (gated stream), negotiated-max adoption, DrainEvents zero-bound.
Worker builds x86 only — verified on windev.
Proto foundation + gateway-side of the size/backpressure pass:
- IPC-02: add GatewayHello.max_frame_bytes (regen Generated/); gateway sends
its negotiated worker-frame max in the handshake so the worker can adopt it
instead of a hard-coded default. Worker read-half lands separately.
- IPC-03: give the pipe frame max envelope-overhead headroom above the public
gRPC cap (WorkerFrameProtocolOptions.EnvelopeOverheadReserveBytes = 64 KiB;
default Worker.MaxMessageBytes bumped to 16 MiB + reserve), cross-validate the
headroom at startup, and pre-check command envelope size in WorkerClient so an
oversized command fails only that correlation (ResourceExhausted) instead of
faulting the whole session.
- IPC-04: reject DrainEvents max_events above a public ceiling in the request
validator (worker per-reply cap lands with the worker half).
Docs: GatewayConfiguration, WorkerFrameProtocol, gateway.md.
Tests: headroom validation, DrainEvents bound, oversized-command per-command
failure (pipe-harness, verified on windev).
Full-host tests passed in isolation but failed 2-4/run under parallel execution on
Windows with 'the process cannot access the file ... because it is being used by
another process' (macOS never sees it — Unix deletes open files). Two shared-state
parallel collisions, discovered while verifying TST-08:
1. Self-signed cert generation. GatewayTlsBootstrapTests sets process-global
Kestrel/TLS env vars that GatewayApplication.Build reads; a parallel host-building
test inherits them mid-run and both generate a cert at the same path, racing on the
fixed-name '<path>.tmp'. Fixes:
- SelfSignedCertificateProvider: stage the PFX in a unique '<path>.<guid>.tmp' instead
of a fixed name, so concurrent/interrupted writers never collide on the temp file
(real robustness: two instances or a restart-during-write no longer clash). The
final atomic Move (last-writer-wins) still yields an equivalent cert.
- TestHostEnvironmentInitializer: default MxGateway__Tls__SelfSignedCertPath to a
per-process temp path so the suite never writes the shared ProgramData default or
fights the deployed service; first host-building test generates, the rest load it.
- GatewayTlsBootstrapTests: [Collection] with DisableParallelization so its global
env-var mutation cannot bleed into parallel tests (new GlobalEnvironmentCollection).
2. AuthStoreHealthCheckTests opened a real SQLite file; Microsoft.Data.Sqlite's
connection pool keeps the .db handle open after 'await using', so the finally
File.Delete threw. Reuse the existing TempDatabaseDirectory helper, which calls
SqliteConnection.ClearAllPools() before deleting.
Server build clean; affected classes 17/17 on macOS. Windows full-suite verified separately.
Empirically verified the full ZB.MOM.WW.MxGateway.Tests suite exits cleanly:
- macOS: 0 surviving testhost after a full run.
- windev (origin/main worktree): 0 new testhost and 0 new worker processes
after a full run, isolated by process StartTime.
Static audit confirms the prime-suspect fixtures already dispose deterministically
(WorkerClient cancels _stopCts + awaits its read/write/heartbeat tasks with a 5s
timeout; GatewaySession disposes the client; harness/E2E fixtures use await using).
Drop the stale 'leaves orphaned testhost processes' rationale from CLAUDE.md's
Source Update Workflow; keep filtered-run guidance as a speed guideline only.
Separately discovered (tracked in 00-tracking.md change log, NOT part of TST-08):
Windows-only temp-file-lock flakiness in ~4 full-host test classes (Sqlite
connection-pool retaining a temp .db handle; SelfSignedCertificateProvider's
fixed-name gw.pfx.tmp racing teardown) that macOS never surfaces.
- SEC-02: DashboardAuthorizationHandler restricts the loopback and
Authentication:Mode=Disabled bypasses to read-only. They now satisfy only a
Viewer-bearing requirement (AnyDashboardRole), never AdminOnly, so anonymous
localhost can view the dashboard but cannot reach API-key CRUD or session
Close/Kill at the policy layer (previously guarded only by service re-checks).
- SEC-12: DashboardSessionAdminService emits canonical AuditEvents through
IAuditWriter (actions dashboard-close-session / dashboard-kill-worker, category
SessionAdmin) on Success/Failure/Denied, mirroring the API-key audit path so
destructive session actions leave durable, queryable rows.
- SEC-20: drop the unbounded session_id tag from the exported
mxgateway.heartbeats.failed counter (per-session detail stays in the snapshot/log).
Docs updated same-change: CLAUDE.md (read-only loopback + 5-min bearer),
GatewayDashboardDesign.md (bypass scoping + session-admin audit), Metrics.md.
Server build clean; 30/30 targeted + 295/295 Dashboard/Security/App/Metrics sweep.
SEC-07: add QueryActiveAlarmsRequest -> events:read scope arm; fix two tests that
constructed StreamAlarmsRequest instead of QueryActiveAlarmsRequest.
SEC-05: shorten hub-token lifetime 30m -> 5m; document that the ?access_token= query
carriage must never be request-logged.
SEC-08: gateway-side CachingApiKeyVerifier (short TTL, keyed on a hash of the presented
secret) skips the per-call store read+last_used write; CoalescingMarkApiKeyStore bounds
last_used writes to <=1/key/min; identity constraints are cached. Invalidation is wired
at the gateway admin sites (revoke/rotate/delete); short TTL backstops out-of-process CLI.
SEC-11: fixed-window rate limit on POST /auth/login + a per-peer (key-id) failure limiter
checked before VerifyAsync; new MxGateway:Security options bound + validated.
Also fixes regressions from the SEC-01/04/06 commit (c185f62) that a narrow test filter
missed (all now covered by a full-suite checkpoint):
- Rooting check is cross-platform: accepts Windows C:\/UNC forms on Unix so the shipped
appsettings path validates on the macOS dev box, still rejecting bare filenames.
- AddGatewayConfiguration TryAdds a non-production IHostEnvironment fallback so the validator
resolves in minimal test/tooling containers; the real host + apikey CLI register the actual
environment first (TryAdd no-op there).
- Test assembly defaults ASPNETCORE_ENVIRONMENT=Development (ModuleInitializer) so full-host
tests exercise wiring instead of tripping the SEC-04/06 production guards.
- GatewayOptionsTests asserts the SEC-01 CommonApplicationData-derived default (platform-correct).
archreview: SEC-07/05/08/11 (P1). Verified: NonWindows build clean; full gateway suite
747 passed / 42 failed, where all 42 are the pre-existing macOS named-pipe-harness failures
(Unix-socket path limit) and 0 are validation/regression failures.
- SEC-01: derive AuthenticationOptions.SqlitePath / TlsOptions.SelfSignedCertPath
defaults from SpecialFolder.CommonApplicationData (was Windows-literal strings
that became CWD-relative files off-Windows); validator rejects non-rooted
overrides; delete the stray C:\ProgramData\...gateway-auth.db files that
materialized under src/; add a tree-hygiene test failing on any *.db under src/.
- SEC-04: GatewayOptionsValidator fails startup when IsProduction() && DisableLogin.
- SEC-06: validator rejects LDAP Transport=None in Production; document the
MxGateway__Ldap__ServiceAccountPassword env override + dev-credential rotation.
Galaxy SnapshotCachePath rooting could not be validator-enforced (bound by the
external GalaxyRepository package, not GatewayOptions) — documented instead.
archreview: SEC-01/04/06 (P1). Verified on the Auth-0.1.4 baseline: Server build
clean, GatewayOptionsValidator + GatewayTreeHygiene tests 53/53.
Bump all four ZB.MOM.WW.Auth.* package refs 0.1.2 -> 0.1.4. The shared
ApiKeyVerifier now rejects any key whose ExpiresUtc is in the past;
existing keys have NULL expiry (never expire), so nothing breaks, and the
auth SQLite DB auto-migrates to schema v3 (nullable expires_utc column) on
first boot. Implement the two IApiKeyAdminStore members added in 0.1.3
(SetScopesAsync/SetEnabledAsync) on the test FakeApiKeyAdminStore.
Build green; no new test failures (the macOS worker-pipe IPC failures are
pre-existing/environmental, identical to the pre-bump baseline).
- IPC-01: regenerate the stale client descriptor set and add ClientProtoInputTests
(semantic, protoc-free) so a missing contract symbol fails the build.
- IPC-20: make publish-client-proto-inputs.ps1 -Check source_code_info-normalized
so it is protoc-version tolerant.
- IPC-19: document + guard the "Generated/ must be committed for net48" rule
(docs/Contracts.md, csproj comment, check-codegen.ps1).
- IPC-09: harden the per-client generate-proto scripts (PATH resolution + pinned
grpcio/protobuf/java version asserts) so regeneration is reproducible.
- TST-03: add .gitea/workflows/ci.yml (portable/java/windows/live jobs) running the
build, tests, client checks, and the codegen guards.
- Also: check-codegen.ps1 Check 3 guards the CLI-02 vendored Rust protos against drift.
archreview: IPC-01/09/19/20 Done, TST-03 In review (pipeline authored + validated,
not yet run on a Gitea runner). Verified on macOS: NonWindows build clean,
ClientProtoInputTests 5/5, -Check exit 0.
build.rs resolved the .proto files via a monorepo-relative path and packaging
relied on `cargo publish --no-verify` to hide the resulting failure. Vendor the
three canonical protos into clients/rust/protos/, resolve them via
CARGO_MANIFEST_DIR (in-repo path preferred, vendored fallback for the packaged
crate), ship them via Cargo.toml `include`, and drop `--no-verify` from
pack-clients.ps1.
archreview: CLI-02 (P1). Verified: cargo fmt/check/test (28) + clippy -D warnings,
and `cargo package` (no --no-verify) compiles standalone in the isolated staging
dir — the out-of-repo proof. Vendored-proto drift is guarded by check-codegen.ps1.
Interlocking changes across the gateway server (shared GatewaySession.cs /
SessionManager.cs), committed together:
- GWC-01 (Critical): alarm monitor now attaches as an internal
(non-counted) distributor subscriber instead of a second raw drain of the
single worker event channel; WorkerClient._events -> SingleReader with a
claimed-once guard so a future dual-consumer regression throws loudly.
- GWC-02 (High): faulted sessions are swept in CloseExpiredLeasesAsync
(IsFaultedReapable + FaultedReason); new FaultedGraceSeconds (default 0).
- GWC-03 (High): configurable MaxSparseArrayLength (default 1_000_000)
enforced before allocation.
- TST-02 (High, security): StreamEvents attach now enforces the opening key
id -> PermissionDenied on owner mismatch.
- TST-12 (Medium): CLAUDE.md retention-defaults sentence corrected.
Verified: NonWindows build clean; targeted tests 135/135 on macOS, plus
WorkerClientTests 18/18 on the Windows host.
A long legitimate ReadBulk pumped Windows messages without refreshing
LastStaActivityUtc, so the watchdog false-positived StaHung past
HeartbeatStuckCeiling and then silently dropped every reply. PumpPendingMessages()
now calls MarkActivity() after pumping; a genuinely stuck STA (no pumping)
still accrues staleness and faults correctly. No MXAccess parity change.
archreview: WRK-01 (P0). Verified on the Windows host (x86): worker builds
clean, StaRuntimeTests + WorkerPipeSessionTests 33/33 pass.
Typed write/command wrappers now call ensure_mxaccess_success after
ensure_command_success, failing on hresult < 0 (COM-correct, matching
CLI-08/Python) or any MxStatusProxy.success == 0, via a new boxed
Error::MxAccess variant with credential-safe formatting. The raw invoke
escape hatch stays unvalidated.
archreview: CLI-03 (P0). Verified: cargo fmt/check/test (28) and clippy --all-targets -D warnings clean.