Commit Graph

607 Commits

Author SHA1 Message Date
Joseph Doherty f1d9ad5ca2 docs(archreview): CLI-15/CLI-04/CLI-30/TST-01 -> Done (Java completes 5/5)
Session-resilience epic Phase 3 fully closed (reconnect protocol + client
consumers across all five clients + server e2e test).

Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 16:57:47 -04:00
Joseph Doherty 1cc0fa4007 feat(java-client): CLI-15 ReplayGap surface + CLI-04 typed-command parity (5/5 complete)
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
2026-07-09 16:57:13 -04:00
Joseph Doherty 908951be9d docs(archreview): E2 CLI-04 In progress (4/5) + CLI-30 Done; Java now local
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
2026-07-09 16:44:24 -04:00
Joseph Doherty bde042b4d4 feat(clients): CLI-04 typed single-item command parity (+CLI-30 unregister) — 4/5 clients
Every parity-critical single-item MXAccess command now has a typed session
helper instead of only a raw-Invoke escape hatch. Added per client:
- Phase 1: AdviseSupervisory, WriteSecured, WriteSecured2, AuthenticateUser,
  ArchestrAUserToId
- Phase 2: AddBufferedItem, SetBufferedUpdateInterval, Suspend, Activate
- CLI-30: Unregister (Rust + .NET; Go/Python already had it)

Each wraps the existing raw-command machinery (no new wire surface) and runs the
same MXAccess-level reply validation (hresult < 0 + MxStatusProxy). MXAccess
parity preserved: WriteSecured before AuthenticateUser+AdviseSupervisory surfaces
the native failure unchanged (not pre-validated/reordered). Credentials
(AuthenticateUser password, WriteSecured payloads) route through each client's
secret-redaction seam and never reach logs/exceptions/ToString/Debug/Display;
each suite asserts a distinctive credential is absent from surfaced errors. New
CLI subcommands source credentials via flag/env, never echoed.

- .NET: 21 helpers (validated + Raw), CLI subcommands, multi-secret CLI redactor.
  Build clean (0 warn), 102 passed.
- Go: 9 helpers + *Raw variants, redactSecrets seam, promoted CLI advise-supervisory
  to typed. gofmt/vet/build/test clean.
- Rust: 10 helpers incl. unregister; verified ensure_mxaccess_success runs on
  secured paths; error.rs credential scrub. fmt/check/test/clippy clean.
- Python: 9 async helpers, redact_secret seam + _invoke_redacted, CLI commands.
  145 passed.
- Shared doc: ClientLibrariesDesign "Typed Command Parity" section.

Java client typed parity is batched to windev (no local JRE); CLI-04 + CLI-30
stay open until it lands.

Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 16:41:43 -04:00
Joseph Doherty 4090a478c8 docs(archreview): TST-04 -> Done (epic governance decision landed)
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 16:33:51 -04:00
Joseph Doherty ed3c6c61c5 docs(archreview): TST-04 governance — Phase 3 done, Phase 4 scoped, Phase 5 deferred
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
2026-07-09 16:33:25 -04:00
Joseph Doherty ef498c80c2 docs(archreview): E1 reconnect-replay — CLI-15 + TST-01 In progress
CLI-15 (4/5 clients, commit 0c6e5b3) and TST-01 server e2e test (commit fed0685)
landed; both gated on the Java client (windev batch) before flipping to Done.

Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 16:27:47 -04:00
Joseph Doherty fed0685633 test(gateway): TST-01 end-to-end reconnect/replay integration test
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
2026-07-09 16:26:40 -04:00
Joseph Doherty 0c6e5b3ace feat(clients): CLI-15 surface ReplayGap reconnect sentinel as typed signal (4/5 clients)
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
2026-07-09 16:22:28 -04:00
Joseph Doherty c823a7b60b docs(archreview): mark WRK-06/11/12 + IPC-15 Done (P2 worker hot-path, windev-verified)
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 16:00:29 -04:00
Joseph Doherty f61c816acf perf(worker): event hot-path allocation + flush cuts (WRK-06/11/12, IPC-15)
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
2026-07-09 15:59:45 -04:00
Joseph Doherty 73ce824f6c docs(archreview): mark TST-11/WRK-15/TST-23 Done (P2 quick items)
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 15:33:52 -04:00
Joseph Doherty ec6f82b124 fix(versioning,docs): stamp .NET assembly version + worker doc drift (TST-11, WRK-15)
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
2026-07-09 15:33:22 -04:00
Joseph Doherty 579282f015 docs(archreview): mark SEC-25/SEC-30 Done (P2 dashboard value redaction + value-log doc)
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 15:29:12 -04:00
Joseph Doherty e15c3cb052 fix(dashboard): redact mirror tag values when ShowTagValues off; trim value-log doc (SEC-25, SEC-30)
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
2026-07-09 15:28:24 -04:00
Joseph Doherty 96702321b1 docs(archreview): mark gateway hot-path P2 findings Done (GWC-06/07/08/14/15, IPC-05/13/14)
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 15:19:38 -04:00
Joseph Doherty cf1b3d40a5 perf(gateway): replay ring LinkedList -> circular array (GWC-14)
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
2026-07-09 15:17:11 -04:00
Joseph Doherty 5e2e40a927 perf(gateway): trim event/command hot-path allocations (GWC-06/07/15, IPC-05)
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
2026-07-09 15:12:43 -04:00
Joseph Doherty baae59ce3b perf(gateway): pool+coalesce worker pipe frame I/O (GWC-08, IPC-13, IPC-14)
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
2026-07-09 14:58:09 -04:00
Joseph Doherty 34aede6767 docs(archreview): mark SEC-03 Done (P2 dashboard __Host- cookie prefix)
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 14:53:47 -04:00
Joseph Doherty 7d42c85345 fix(dashboard): conditionally restore __Host- cookie prefix (SEC-03)
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
2026-07-09 14:53:19 -04:00
Joseph Doherty 5a3951bcfd docs(archreview): mark P2 Wave A findings Done (13: doc-drift + version alignment)
Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 14:47:11 -04:00
Joseph Doherty 06e1046317 docs(archreview): P2 doc-drift sweep (IPC-06/07/17/21, SEC-09/22, CLI-12/16, TST-13)
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
2026-07-09 14:46:15 -04:00
Joseph Doherty 4fe1917aa6 fix(clients): align client version constants to package manifests (CLI-18/21/26/29)
- .NET: add explicit <Version>0.1.2</Version> to client csproj (CLI-18)
- Go: ClientVersion 0.1.0-dev -> 0.1.2 (CLI-21)
- Python: __version__ 0.1.0 -> 0.1.2 to match pyproject.toml (CLI-26)
- Rust: CLIENT_VERSION now sourced from env!(CARGO_PKG_VERSION) so it
  cannot drift from Cargo.toml (CLI-29)

Verified: dotnet build clean; go build/test ok; cargo check/test/clippy
clean; pytest 127 passed. P2 client version-drift cluster.

Claude-Session: https://claude.ai/code/session_01DMXXvNuPekkkrTEyPNxEkW
2026-07-09 14:43:03 -04:00
dohertj2 69ea7937ca Merge pull request 'Architecture remediation: P1 tier (process & hardening)' (#121) from fix/archreview-p1 into main
ci / windows (push) Waiting to run
ci / live-mxaccess (push) Waiting to run
ci / java (push) Failing after 39s
ci / portable (push) Failing after 47s
2026-07-09 09:59:48 -04:00
dohertj2 b75bfbe8f4 Merge pull request 'fix(archreview): P0 tier remediation (8 findings)' (#120) from fix/archreview-p0 into main 2026-07-09 09:58:55 -04:00
Joseph Doherty a038363e74 feat(security): apikey --expires CLI flag + dashboard expiry/staleness badge (SEC-10 polish)
ci / windows (push) Waiting to run
ci / live-mxaccess (push) Waiting to run
ci / windows (pull_request) Waiting to run
ci / live-mxaccess (pull_request) Waiting to run
ci / portable (push) Failing after 44s
ci / java (push) Failing after 50s
ci / java (pull_request) Failing after 37s
ci / portable (pull_request) Failing after 55s
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).
2026-07-09 09:48:09 -04:00
Joseph Doherty de57d834b1 docs(archreview-p1): mark GWC-04 Done — Wave 3 complete
ci / windows (push) Waiting to run
ci / live-mxaccess (push) Waiting to run
ci / windows (pull_request) Waiting to run
ci / live-mxaccess (pull_request) Waiting to run
ci / portable (push) Failing after 3m27s
ci / java (push) Failing after 3m30s
ci / java (pull_request) Failing after 46s
ci / portable (pull_request) Failing after 51s
2026-07-09 09:28:39 -04:00
Joseph Doherty 32d6b48910 feat(gateway): decouple worker event enqueue from the read loop (GWC-04)
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.
2026-07-09 09:28:13 -04:00
Joseph Doherty bfbfcdd112 docs(archreview-p1): mark IPC-02/03/04 + WRK-04/07 Done (Wave 3, windev-verified) 2026-07-09 09:21:48 -04:00
Joseph Doherty 309296f467 test(ipc): refresh proto descriptor set + MaxMessageBytes default expectation
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.
2026-07-09 09:16:31 -04:00
Joseph Doherty ebe6aeac98 feat(worker): adopt negotiated frame max, bound drain, priority write scheduler (IPC-02/04 + WRK-04/07 worker half)
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.
2026-07-09 09:09:14 -04:00
Joseph Doherty c8b3a2281a feat(ipc): negotiate worker frame max, add gRPC headroom, bound DrainEvents (IPC-02/03/04 gateway half)
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).
2026-07-09 08:49:59 -04:00
Joseph Doherty e1a505d662 docs(archreview-p1): record Windows temp-file-lock flakiness fix + windev verification
3 consecutive windev full-suite runs: 793 passed / 2 failed, 0 orphans (was flaky
2-4 fails). The 2 stable failures are pre-existing windev-environmental (cert SAN
machine-name assertion; event-stream concurrency timing) and pass on macOS.
2026-07-09 08:20:43 -04:00
Joseph Doherty 11a716a07f test+fix: eliminate Windows full-suite temp-file-lock flakiness
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.
2026-07-09 08:14:55 -04:00
Joseph Doherty 8c06635ee5 docs(archreview-p1): TST-08 orphaned-testhost leak does not reproduce
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.
2026-07-09 08:02:33 -04:00
Joseph Doherty 74e815c4d7 fix(archreview-p1): SEC-02/12/20 dashboard + observability hardening
- 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.
2026-07-09 07:47:51 -04:00
Joseph Doherty c2df4a0aae docs(archreview): P1 Wave 2b status (SEC-05/07/08/11 Done) 2026-07-09 07:32:14 -04:00
Joseph Doherty 17f16ea181 fix(archreview): security authz+hub cluster (SEC-07/05/08/11) + validation hardening
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.
2026-07-09 07:31:35 -04:00
Joseph Doherty 970613eebd docs(archreview): P1 Wave 2a status (SEC-01/04/06 Done; SEC-10 Done via G-2 Auth 0.1.4) 2026-07-09 06:41:40 -04:00
Joseph Doherty c185f621a4 fix(archreview): security config guards (SEC-01/04/06)
- 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.
2026-07-09 06:40:57 -04:00
Joseph Doherty 197731ae2d chore(auth): consume ZB.MOM.WW.Auth 0.1.4 — gain API-key expiry enforcement (archreview G-2)
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).
2026-07-09 06:37:10 -04:00
Joseph Doherty 8fb1a814d4 docs(archreview): P1 Wave 1 status (IPC-01/09/19/20, CLI-02 Done; TST-03 In review) 2026-07-09 06:18:03 -04:00
Joseph Doherty d5248f61a2 fix(archreview): CI pipeline + codegen freshness guards (IPC-01/09/19/20, TST-03)
- 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.
2026-07-09 06:17:56 -04:00
Joseph Doherty 219fa6ddb4 fix(CLI-02): make Rust crate buildable outside the repository
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.
2026-07-09 06:17:56 -04:00
Joseph Doherty f726458086 docs(archreview): mark P0 tier Done in remediation tracker
All 8 P0 findings (GWC-01/02/03, WRK-01, CLI-01, CLI-03, TST-02, TST-12)
recorded as Done with verification notes and change-log entries.
2026-07-09 05:51:57 -04:00
Joseph Doherty 20392cf246 fix(archreview): gateway core P0 remediation (GWC-01/02/03, TST-02, TST-12)
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.
2026-07-09 05:51:57 -04:00
Joseph Doherty 31eec41456 fix(WRK-01): STA pump step refreshes activity to prevent false StaHung
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.
2026-07-09 05:51:45 -04:00
Joseph Doherty 5faef6c012 fix(CLI-03): Rust typed invoke validates HRESULT/MXSTATUS_PROXY
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.
2026-07-09 05:51:45 -04:00
Joseph Doherty 1eb00276bc fix(CLI-01): Go Events() emits terminal ErrSlowConsumer on overflow
Events()/EventsAfter() previously closed the channel silently when the
16-slot buffer overflowed, indistinguishable from a graceful server end.
Reserve one slot and emit a terminal EventResult wrapping the new exported
ErrSlowConsumer so overflow is always observable. The blocking
SubscribeEvents path (gRPC flow-controlled) is unchanged.

archreview: CLI-01 (P0). Verified: gofmt clean, go build ./..., go test ./... (+ -race) pass.
2026-07-09 05:51:45 -04:00