From ead921cace3f69ef0d7b08de4a417fae4fd62fe9 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 7 Aug 2026 01:57:01 -0400 Subject: [PATCH] =?UTF-8?q?docs:=20truth=20sweep=20=E2=80=94=20Galaxy=20ad?= =?UTF-8?q?option,=20Auth=200.1.5,=20redaction=20seam,=20resolved=20A2=20c?= =?UTF-8?q?aveats?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Claude-Session: https://claude.ai/code/session_014WNM4vjoVksyyBraTXSZE1 --- A2-galaxyrepository-adoption-handoff.md | 20 +++++---- .../2026-07-12/40-security-dashboard.md | 2 +- .../remediation/40-security-dashboard.md | 2 +- archreview/remediation/00-tracking.md | 4 +- docs/Diagnostics.md | 4 +- docs/GalaxyRepository.md | 44 +++++++++++++------ stillpending.md | 4 +- 7 files changed, 50 insertions(+), 30 deletions(-) diff --git a/A2-galaxyrepository-adoption-handoff.md b/A2-galaxyrepository-adoption-handoff.md index 88ec33c..a99a233 100644 --- a/A2-galaxyrepository-adoption-handoff.md +++ b/A2-galaxyrepository-adoption-handoff.md @@ -194,15 +194,17 @@ be **deleted**. **Keep** the mxaccessgw-specific ones that exercise behavior the `PersistSnapshot`, but the deployments must carry `MxGateway__Galaxy__SnapshotCachePath` and `MxGateway__Galaxy__PersistSnapshot` in their NSSM env on redeploy, or snapshot persistence silently no-ops in production. -- **Pre-existing NU1903 (unrelated):** adding the package surfaced a transitive `SQLitePCLRaw.lib.e_sqlite3` - 2.1.11 advisory (GHSA-2m69-gcr7-jv3q, no upstream patch) that breaks the build under `TreatWarningsAsErrors` - — already red on `main`. Resolved with a targeted `NuGetAuditSuppress` in `src/Directory.Build.props` - (its own commit). Remove the suppression once a patched e_sqlite3 ships. -- **Pre-existing IntegrationTests break (unrelated, NOT fixed here):** `IntegrationTests/WorkerLiveMxAccessSmokeTests.cs` - constructs `EventStreamService` with 6 ctor args, but a prior event-stream refactor reduced that ctor — so - the IntegrationTests project does not compile (already broken on `main`, independent of Galaxy). The Galaxy - live tests there were rebound to the lib and compile in isolation, but the project won't build until that - unrelated call site is fixed. Track separately. +- **Pre-existing NU1903 (unrelated) — ✅ RESOLVED (2026-07-18, commit `2f0cfe3`):** adding the package surfaced a transitive `SQLitePCLRaw.lib.e_sqlite3` + 2.1.11 advisory (GHSA-2m69-gcr7-jv3q, at the time no upstream patch) that breaks the build under `TreatWarningsAsErrors` + — already red on `main`. Initially resolved with a targeted `NuGetAuditSuppress` in `src/Directory.Build.props` + (its own commit). The patched e_sqlite3 (2.1.12) has since shipped: the suppression was **removed** and the + patched native lib pinned intentionally (`src/Directory.Build.props` now documents this in place of the suppression). +- **Pre-existing IntegrationTests break (unrelated, NOT fixed here) — ✅ RESOLVED since:** `IntegrationTests/WorkerLiveMxAccessSmokeTests.cs` + constructed `EventStreamService` with 6 ctor args, but a prior event-stream refactor reduced that ctor — so + the IntegrationTests project did not compile (already broken on `main`, independent of Galaxy). The call site + has since been fixed to match the 3-arg ctor (`sessionManager, options, metrics` — + `Grpc/EventStreamService.cs:11-14`; call site at `WorkerLiveMxAccessSmokeTests.cs:~1580`) and the project compiles + (verified 2026-08-07). - **No republish needed for the lib test additions:** the browse-projector / deploy-notifier / refresh-service tests were added to the lib AFTER 0.2.0 was published; tests aren't shipped, so 0.2.0 is unchanged. diff --git a/archreview/2026-07-12/40-security-dashboard.md b/archreview/2026-07-12/40-security-dashboard.md index 5f85029..05ba406 100644 --- a/archreview/2026-07-12/40-security-dashboard.md +++ b/archreview/2026-07-12/40-security-dashboard.md @@ -14,7 +14,7 @@ | SEC-06 | Done | **Partial** | Production hard-stop verified: `GatewayOptionsValidator.cs:161-165` (`Transport==None` in Production → startup error). Docs: `docs/GatewayConfiguration.md:244-248` (env-var override `MxGateway__Ldap__ServiceAccountPassword` documented). | **The committed dev service-account password is still in the repo at `appsettings.json:29`** (`Ldap.ServiceAccountPassword`) and has not been rotated — the doc itself says it "should be rotated". The transport guard shipped; the credential-removal/rotation half of the remediation did not. → **SEC-36** | | SEC-07 | Done | **Yes** | `Security/Authorization/GatewayGrpcScopeResolver.cs:23` (`QueryActiveAlarmsRequest => GatewayScopes.EventsRead`); both tests now construct the real type (`Tests/Security/Authorization/GatewayGrpcAuthorizationInterceptorTests.cs:330,350`). | | | SEC-08 | Done | **Yes** | `Security/Authentication/CachingApiKeyVerifier.cs` (15 s success-only TTL cache keyed on SHA-256 of the presented token, `:96-120`; only successes cached `:110-117`); `Security/Authentication/CoalescingMarkApiKeyStore.cs:76-113` (≤1 `last_used` write/key/60 s); wired as decorators in `Security/Authentication/AuthStoreServiceCollectionExtensions.cs:89-98`; invalidation on dashboard revoke/rotate/delete at `Dashboard/DashboardApiKeyManagementService.cs:104,144,190`; per-call constraints JSON deserialize removed via blob cache (`Security/Authentication/GatewayApiKeyIdentityMapper.cs:22-45`). Tests exist (`Tests/Security/Authentication/CachingApiKeyVerifierTests.cs`). | New surface reviewed in depth — see SEC-34 (staleness/race, bounded) below. | -| SEC-10 | Done | **Yes** | CLI: `--expires` parsed as relative `d`/`h` or absolute ISO-8601 with `AssumeUniversal|AdjustToUniversal` (`Security/Authentication/ApiKeyAdminCommandLineParser.cs:242-274`), threaded into `CreateKeyAsync` (`:85-117`). Dashboard: `DashboardApiKeySummary.cs:13` (`ExpiresUtc`), snapshot projection `Dashboard/DashboardSnapshotService.cs:277`, badge logic compares against `DateTimeOffset.UtcNow` with a 7-day "Expiring" warn window (`Dashboard/Components/Pages/ApiKeysPage.razor:474-497`; `StatusBadge.razor:12-13`). Verifier-side rejection is in the shared `ZB.MOM.WW.Auth.ApiKeys` 0.1.4 (documented `docs/Authentication.md:64-65`; not readable in this repo). | UTC semantics are correct end-to-end on the gateway side. Boundary note: `expiresAt <= now` shows Expired, and relative parse rejects signed values (`NumberStyles.None`). A just-expired key can still authenticate for ≤15 s via the verification cache — see SEC-34. | +| SEC-10 | Done | **Yes** | CLI: `--expires` parsed as relative `d`/`h` or absolute ISO-8601 with `AssumeUniversal|AdjustToUniversal` (`Security/Authentication/ApiKeyAdminCommandLineParser.cs:242-274`), threaded into `CreateKeyAsync` (`:85-117`). Dashboard: `DashboardApiKeySummary.cs:13` (`ExpiresUtc`), snapshot projection `Dashboard/DashboardSnapshotService.cs:277`, badge logic compares against `DateTimeOffset.UtcNow` with a 7-day "Expiring" warn window (`Dashboard/Components/Pages/ApiKeysPage.razor:474-497`; `StatusBadge.razor:12-13`). Verifier-side rejection is in the shared `ZB.MOM.WW.Auth.ApiKeys` 0.1.5 (pins bumped 0.1.4→0.1.5 in `e107019`, a transitive-dependency security fix only — the expiry enforcement is unchanged; documented `docs/Authentication.md:64-65`; not readable in this repo). | UTC semantics are correct end-to-end on the gateway side. Boundary note: `expiresAt <= now` shows Expired, and relative parse rejects signed values (`NumberStyles.None`). A just-expired key can still authenticate for ≤15 s via the verification cache — see SEC-34. | | SEC-11 | Done | **Yes, with new defects** | Login: fixed-window per-remote-IP limiter policy (`Dashboard/DashboardEndpointRouteBuilderExtensions.cs:27-41`), applied to POST `/auth/login` (`:83`), registered + 429 (`GatewayApplication.cs:111-126`), middleware in pipeline (`GatewayApplication.cs:46`), test `Tests/Gateway/Dashboard/DashboardLoginRateLimitTests.cs`. gRPC: `Security/Authorization/ApiKeyFailureLimiter.cs` checked **before** the store read (`GatewayGrpcAuthorizationInterceptor.cs:72-77`), failure recorded `:89`, reset on success `:97`; interceptor test asserts the short-circuit (`GatewayGrpcAuthorizationInterceptorTests.cs:395-402`). | The limiter exists and is enforced, but its key-id partitioning creates an unauthenticated lockout DoS (**SEC-31**) and its LRU eviction is flushable (**SEC-32**). | | SEC-12 | Done | **Yes** | `Dashboard/DashboardSessionAdminService.cs`: canonical `AuditEvent`s `dashboard-close-session`/`dashboard-kill-worker` (`:37-40`), written on Denied (`:65,147`), Success (`:89-96,171-178`), and every Failure arm (`:105,116,132,187,198,214`), category `SessionAdmin`, actor/remote/correlation captured (`:238-261`), via `IAuditWriter` (same store as API-key events). | Audit-event completeness is good: denied, not-found, faulted, and unexpected paths all emit. | | SEC-20 | Done | **Yes** | `Metrics/GatewayMetrics.cs:374` — `_heartbeatFailuresCounter.Add(1)` with no `session_id` tag (rationale comment `:370-373`). | The in-memory per-session map remains dashboard-only. | diff --git a/archreview/2026-07-12/remediation/40-security-dashboard.md b/archreview/2026-07-12/remediation/40-security-dashboard.md index 938547c..c17fd70 100644 --- a/archreview/2026-07-12/remediation/40-security-dashboard.md +++ b/archreview/2026-07-12/remediation/40-security-dashboard.md @@ -145,7 +145,7 @@ Post-run, verify no new `C:\*` file exists under any `bin/` (manual `find src -n **Design.** -- **Expiry (window 2): eliminate, don't document.** The shared `ApiKeyIdentity` (0.1.4) carries `ExpiresUtc`; when caching a success, cap the entry lifetime at the key's expiry: `AbsoluteExpiration = min(now + ttl, ExpiresUtc)` (skip caching entirely if already ≤ now). A cached hit can then never outlive the key. Confirm during implementation that the library verifier populates `ExpiresUtc` on the returned identity; if it does not, fall back to documenting the ≤ TTL window in the remarks and `docs/Authentication.md` and file a donor-library ask. +- **Expiry (window 2): eliminate, don't document.** The shared `ApiKeyIdentity` (0.1.5 — bumped from 0.1.4 in `e107019`, no API change) carries `ExpiresUtc`; when caching a success, cap the entry lifetime at the key's expiry: `AbsoluteExpiration = min(now + ttl, ExpiresUtc)` (skip caching entirely if already ≤ now). A cached hit can then never outlive the key. Confirm during implementation that the library verifier populates `ExpiresUtc` on the returned identity; if it does not, fall back to documenting the ≤ TTL window in the remarks and `docs/Authentication.md` and file a donor-library ask. - **Invalidate race (window 3): per-key generation check.** `ConcurrentDictionary _generations`; `Invalidate(keyId)` increments the generation **before** evicting cache keys. `VerifyAsync` parses the key id from the token up front (same split the interceptor does — cheap, no store access), snapshots `g0` before calling the inner verifier, and after a success only `Set`s when the generation still equals `g0` — then re-reads the generation after the `Set` and self-evicts if it moved (bump-before-evict + set-then-recheck closes the remaining interleaving). Unparseable tokens skip caching already (`TryComputeCacheKey`). - **CLI window (1): accept and keep documented** — cross-process invalidation is out of scope by design; the TTL is the backstop and the remarks already say so. diff --git a/archreview/remediation/00-tracking.md b/archreview/remediation/00-tracking.md index 1887bf8..1720557 100644 --- a/archreview/remediation/00-tracking.md +++ b/archreview/remediation/00-tracking.md @@ -236,7 +236,7 @@ Full design + implementation for each row lives in the linked domain doc under i | TST-21 | Low | — | S | — | Not started | Log rotation configured but minimal | | TST-22 | Low | — | S | — | Not started | Config-shape JSON block omits documented keys | | TST-23 | Low | P2 | S | — | Done | Bidirectional `Session` RPC never built | -| TST-24 | Low | P2 | M | TST-03 | Not started | Client wire behaviour has no automated verification | +| TST-24 | Low | P2 | M | TST-03 | Not started | Client wire behaviour has no automated verification. **Gate cleared:** TST-03 CI is Done (live and green 2026-07-10; Windows/x86 tier green 2026-07-13 via the TST-25/TST-26 SSH-driven windev job), so TST-24 is unblocked — deferred by choice now, not CI-gated | ## Cross-cutting clusters @@ -276,7 +276,7 @@ Findings the review flagged as one coordinated design pass — sequence them tog | 2026-07-09 | P1 Wave 3 (size/backpressure topology + write ordering). IPC-02/03/04 + WRK-04/07 → `Done`. **Size negotiation (IPC-02):** added `GatewayHello.max_frame_bytes` (regen `Generated/` + `clients/proto` descriptor refresh with pinned protoc 34.1); the gateway sends its negotiated worker-frame max and the worker adopts it (`WorkerFrameProtocolOptions.AdoptNegotiatedMaxMessageBytes`, 0 = keep default, >256 MiB rejected) instead of a hard-coded default. **Headroom (IPC-03):** the pipe frame max now sits `EnvelopeOverheadReserveBytes` (64 KiB) above the public gRPC cap (default `Worker.MaxMessageBytes` 16 MiB→16 MiB+64 KiB), cross-validated at startup; `WorkerClient` pre-checks command envelope size and fails only the offending correlation (`ResourceExhausted`) instead of `SetFaulted`ing the session. **Drain bound (IPC-04):** gateway request validator rejects `DrainEvents max_events` above 10 000; the worker caps each reply at `MaxDrainEventsPerReply` (10 000) and treats `max_events = 0` as that cap, not "drain all". **Sequence (WRK-04):** `WorkerFrameWriter` stamps the envelope `Sequence` at the point of writing under the write lock, so wire order and stamped sequence always agree under concurrent producers. **Priority (WRK-07):** the worker writer is now a cooperative priority scheduler — control frames (reply/fault/heartbeat/shutdown-ack) drain ahead of event frames; per-frame validation/size rejections fail only that frame, a stream failure fails all queued. Docs same-change (GatewayConfiguration, WorkerFrameProtocol, gateway.md). **Verified:** macOS NonWindows build clean + validator/grpc tests green; **windev** x86 worker builds clean, `Worker.Tests` 352 passed / 0 failed / 11 skipped (incl. new monotonic-sequence, control-before-event priority, negotiated-max, drain-bound tests), gateway `Tests` 799 passed / 3 failed — all 3 pre-existing windev-environmental (SelfSigned SAN + 2 `EventStreamServiceTests` timing, both pass in isolation). Commits `c8b3a22` (gateway half), `ebe6aea` (worker half), `309296f` (descriptor + default-expectation refresh). GWC-04 (event-channel decoupling) is the remaining Wave 3 item. | | 2026-07-09 | P1 S-misc (dashboard/observability hardening). SEC-02/12/20 → `Done`. SEC-02: `DashboardAuthorizationHandler` restricts the loopback + `Authentication:Mode=Disabled` bypasses to read-only (they satisfy a Viewer-bearing requirement but never `AdminOnly`), closing the policy-layer gap where anonymous localhost was authorized for Admin surfaces. SEC-12: `DashboardSessionAdminService` now emits canonical `AuditEvent`s (`dashboard-close-session`/`dashboard-kill-worker`, category `SessionAdmin`) through `IAuditWriter` on Success/Failure/Denied, so Close/Kill land durable audit rows. SEC-20: dropped the unbounded `session_id` tag from the exported `mxgateway.heartbeats.failed` counter. Docs updated same-change (CLAUDE.md, GatewayDashboardDesign.md, Metrics.md). Server build clean (0 warnings); targeted classes 30/30 pass; broader Dashboard+Security+GatewayApplication+Metrics sweep 295/295 pass. | | 2026-07-09 | P1 Wave 2b (security authz+hub). SEC-05/07/08/11 → `Done` (hub-token lifetime; QueryActiveAlarms scope arm; gateway-side verification cache + last-used coalescing; login rate limit + per-peer gRPC failure limiter). Full-suite checkpoint caught + fixed regressions the earlier narrow SEC-01/04/06 filter missed: cross-platform path-rooting, an `IHostEnvironment` fallback for minimal DI containers, a test-assembly `ASPNETCORE_ENVIRONMENT=Development` default, and a platform-correct default-path assertion. Suite: 747 passed / 42 failed, all 42 pre-existing macOS named-pipe-harness env failures. | -| 2026-07-09 | P1 Wave 2a (security). SEC-01/04/06 → `Done` (config path-rooting + production validator guards; Server build clean, validator+hygiene tests 53/53). SEC-10 → `Done`: the shared `ZB.MOM.WW.Auth.ApiKeys` gained optional `ExpiresUtc` (expired keys rejected, auth DB auto-migrates to schema v3) via a concurrent HistorianGateway-remediation session's "G-2"; this repo consumes it by bumping the four `Auth.*` refs 0.1.2→0.1.4 (commit 197731a). Remaining SEC-10 polish (`apikey create --expires` + dashboard staleness badge) tracked as a small follow-up. | +| 2026-07-09 | P1 Wave 2a (security). SEC-01/04/06 → `Done` (config path-rooting + production validator guards; Server build clean, validator+hygiene tests 53/53). SEC-10 → `Done`: the shared `ZB.MOM.WW.Auth.ApiKeys` gained optional `ExpiresUtc` (expired keys rejected, auth DB auto-migrates to schema v3) via a concurrent HistorianGateway-remediation session's "G-2"; this repo consumes it by bumping the four `Auth.*` refs 0.1.2→0.1.4 (commit 197731a; since bumped to 0.1.5 in `e107019` — 0.1.4 plus a transitive SQLitePCLRaw security pin, no API change, expiry enforcement retained). Remaining SEC-10 polish (`apikey create --expires` + dashboard staleness badge) tracked as a small follow-up. | | 2026-07-09 | P1 Wave 1 (CI + codegen freshness + Rust buildability) via parallel agents. IPC-01/09/19/20, CLI-02 → `Done`; TST-03 → `In review` (CI pipeline authored + YAML/layout-validated, but not yet executed on a Gitea runner — proven on first push). Verified on macOS: NonWindows build clean, `ClientProtoInputTests` 5/5, `publish-client-proto-inputs.ps1 -Check` exit 0, `cargo package` (no `--no-verify`) compiles standalone. Added a vendored-Rust-proto drift guard (Check 3) to `check-codegen.ps1` closing the CLI-02 static-copy risk. IPC-09 script guards not executed end-to-end (need pinned python/JRE toolchains); logic is PATH-resolution + version assertion. | | 2026-07-09 | WRK-01 → `Done`. Verified on Windows host (windev) via an isolated `origin/main` worktree: worker builds x86 clean, `StaRuntimeTests`+`WorkerPipeSessionTests` 33/33 pass. Fixed an `xUnit1030` build error (the new worker test used `.ConfigureAwait(false)` in `[Fact]` bodies) that the macOS tree could not surface. Also ran GWC-01's Windows-only `WorkerClientTests` on windev: 18/18 pass (incl. `ReadEventsAsync_SecondEnumerator_Throws`). All 8 P0 findings now `Done`. Not yet committed. | | 2026-07-10 | **TST-03 → `Done`: CI is live and green** on branch `fix/ci-selfhosted-tooling`. The pipeline was authored (P1) but had never executed. Root cause it never ran: the co-located `gitea-runner` on the docker host (`10.100.0.35`) spawned job containers on an isolated network (`container.network: ""`) that could not resolve Gitea's internal clone URL `http://gitea:3000`; one-line host fix `container.network: "traefik"` + `docker restart gitea-runner`. The self-hosted `catthehacker` act image also lacks tooling GitHub-hosted runners preinstall — ci.yml now installs pwsh (dotnet global tool, for `check-codegen.ps1`) and Gradle 9.5.1 directly (act can't resolve the `gradle/actions` monorepo action; no gradle wrapper in repo). Driving to green surfaced and fixed **five real latent defects** (TST-03 doing its job): `check-codegen.ps1` `.Trim()`-on-`$null` on a clean tree; **stale vendored rust proto** (`clients/rust/protos/mxaccess_worker.proto` missing canonical `max_frame_bytes`); `OrphanWorkerTerminatorTests` hard-coded `C:\` path failing Linux `Path.GetFullPath` (0 kills); **stale java generated** `MxaccessWorker.java` (missing `max_frame_bytes`, regenerated); a sync python test building a `grpc.aio.Channel` with no current event loop on py3.12 (autouse conftest fixture). Result: `portable` **success** (NonWindows build + codegen freshness + 808/808 gateway tests + .NET/Go/Rust/Python clients) and `java` **success**. `windows` + `live-mxaccess` jobs remain `queued` pending a self-hosted **windev** runner (`10.100.0.48`) with those labels — separate follow-up, does not gate portable/java. | diff --git a/docs/Diagnostics.md b/docs/Diagnostics.md index 185012c..5246c88 100644 --- a/docs/Diagnostics.md +++ b/docs/Diagnostics.md @@ -123,9 +123,9 @@ The split uses `count: 3` because the secret portion may itself contain undersco ### Command value redaction -> **Not yet implemented.** Command-value logging is *not* wired end-to-end. There is no `MxGateway:Diagnostics:LogCommandValues` (or equivalent) configuration knob, and `RedactCommandValue` / `IsCredentialBearingCommand` have no call sites in the gateway — no command values are logged anywhere today, which is the safest posture. The helpers below exist as the intended redaction seam for a future opt-in value-logging feature; that wiring is deferred until secured-bulk command variants are covered by the redactor's credential list (the `WriteSecuredBulk` / `WriteSecured2Bulk` gap), so enabling value logging cannot leak a secured-bulk payload. Until then, treat this section as describing the planned shape, not current behavior. +> **Redaction seam wired; value logging still has no opt-in knob** (updated 2026-08-07). Since commit `47c0b64` (2026-07-27), `RedactCommandValue` **is** wired: `GatewayLogRedactorSeam` (`Diagnostics/GatewayLogRedactorSeam.cs`) calls it for any non-null `CommandValue` log property, gated on `CommandMethod`, so a command payload that reaches a log event is masked on every sink through the shared `ILogRedactor` seam. What remains true from the original note: there is still no `MxGateway:Diagnostics:LogCommandValues` (or equivalent) configuration knob — the seam exposes no opt-in, `valueLoggingEnabled` is never passed `true`, so **every** non-null command value is redacted, credential-bearing or not. In practice no log statement currently emits a `CommandValue` property, but one that does can no longer leak a payload in the clear. -The intended `RedactCommandValue` would enforce the "values are opt-in and redacted by default" rule: +`RedactCommandValue` enforces the "values are opt-in and redacted by default" rule: ```csharp public static object? RedactCommandValue( diff --git a/docs/GalaxyRepository.md b/docs/GalaxyRepository.md index ff61d60..aa1c0a7 100644 --- a/docs/GalaxyRepository.md +++ b/docs/GalaxyRepository.md @@ -1,5 +1,18 @@ # Galaxy Repository Browse +> **Adopted as a shared package (2026-06-25, commit `8e196a7`).** The Galaxy browse +> implementation described in this document no longer lives in this repository. The +> gateway consumes the shared **`ZB.MOM.WW.GalaxyRepository`** library (pinned at +> `0.2.0` from the Gitea NuGet feed; sources hosted in the `scadaproj` repo), wired +> in `GatewayApplication.cs` via `AddZbGalaxyRepository` (registration) and +> `MapZbGalaxyRepository` (endpoint mapping). The former in-repo `Server/Galaxy/` +> classes and `Grpc/GalaxyRepositoryGrpcService.cs` / `Grpc/GalaxyProtoMapper.cs` +> were deleted with the adoption. The wire contract (`galaxy_repository.v1`) is +> unchanged, so the RPC behavior, filters, paging, caching, and snapshot semantics +> below still apply as documented — but any `src/ZB.MOM.WW.MxGateway.Server/...` +> file path cited below is historical; the equivalent class now lives in the +> shared library. + The gateway exposes a read-only browse surface over the AVEVA System Platform Galaxy Repository (the SQL Server database named `ZB`). Clients use it to enumerate the deployed object hierarchy and each object's attributes @@ -107,7 +120,8 @@ server and dashboard views are consistent. ## Hierarchy Cache The gateway holds a single shared `IGalaxyHierarchyCache` -(`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) — every +(`GalaxyHierarchyCache`, library-side in `ZB.MOM.WW.GalaxyRepository` — formerly +`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) — every `DiscoverHierarchy` and `GetLastDeployTime` request reads from this cache rather than hitting SQL. Many clients can browse concurrently with at most one SQL query in flight. @@ -174,7 +188,8 @@ record: deleting it only forces the next cold start to wait for live SQL. ## Deploy Notifications `WatchDeployEvents` is a server-streaming RPC backed by -`IGalaxyDeployNotifier` (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`). +`IGalaxyDeployNotifier` (`GalaxyDeployNotifier`, library-side in +`ZB.MOM.WW.GalaxyRepository`). The notifier maintains a private bounded channel per subscriber so a slow client cannot back-pressure other subscribers or the publisher. @@ -322,7 +337,7 @@ fields cannot express null. Use it to distinguish "no dimension reported" from ```text gRPC client(s) - -> GalaxyRepositoryGrpcService (src/ZB.MOM.WW.MxGateway.Server/Grpc/) + -> Galaxy repository gRPC service (library-side; mapped via MapZbGalaxyRepository) DiscoverHierarchy, GetLastDeployTime, BrowseChildren -> IGalaxyHierarchyCache.Current WatchDeployEvents -> IGalaxyDeployNotifier TestConnection -> GalaxyRepository (direct SQL) @@ -341,41 +356,44 @@ GalaxyHierarchyRefreshService (BackgroundService) -> IGalaxyDeployNotifier.Publish (only on deploy change) ``` -Component breakdown: +Component breakdown (all of these classes are **library-side**, in the shared +`ZB.MOM.WW.GalaxyRepository` package — the in-repo files formerly at the paths +below were deleted in commit `8e196a7`): -- `GalaxyRepository` (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyRepository.cs`) holds +- `GalaxyRepository` (formerly `src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyRepository.cs`) holds the SQL. Both `HierarchySql` and `AttributesSql` walk template-derivation and package-derivation chains via recursive CTEs and pick the most-derived override per object. `HierarchySql` still matches the OtOpcUa original; `AttributesSql` does not — it additionally enumerates built-in primitive attributes (see [Built-in vs configured attributes](#built-in-vs-configured-attributes)). - `GalaxyHierarchyCache` - (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) holds the most + (formerly `src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyCache.cs`) holds the most recent immutable `GalaxyHierarchyCacheEntry` (materialized objects + precomputed dashboard summary + counts + status). All gRPC clients share the same entry. - `GalaxyHierarchyRefreshService` - (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs`) is a + (formerly `src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyHierarchyRefreshService.cs`) is a hosted `BackgroundService` that drives `RefreshAsync` on the configured interval, with deploy-time gating to avoid unnecessary heavy queries. - `GalaxyDeployNotifier` - (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`) is a thin + (formerly `src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyDeployNotifier.cs`) is a thin per-subscriber-channel fan-out for streaming clients. - `GalaxyProtoMapper` - (`src/ZB.MOM.WW.MxGateway.Server/Grpc/GalaxyProtoMapper.cs`) converts row models to + (formerly `src/ZB.MOM.WW.MxGateway.Server/Grpc/GalaxyProtoMapper.cs`) converts row models to proto messages. Used by the cache during refresh to materialize the reply once. - `GalaxyBrowseProjector` - (`src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyBrowseProjector.cs`) projects one level + (formerly `src/ZB.MOM.WW.MxGateway.Server/Galaxy/GalaxyBrowseProjector.cs`) projects one level of children out of an immutable cache entry. Memoizes the filtered child list per cache-entry instance so repeated paging is an O(pageSize) slice rather than an O(siblings) filter scan. The memo is keyed on the cache entry reference, so a new entry from the background refresh makes the stale memo unreachable and it is collected with it. `DashboardBrowseService` wraps this projector to drive the dashboard's lazy-expand tree. -- `GalaxyRepositoryGrpcService` - (`src/ZB.MOM.WW.MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs`) implements - the five RPCs. +- The Galaxy repository gRPC service (formerly + `src/ZB.MOM.WW.MxGateway.Server/Grpc/GalaxyRepositoryGrpcService.cs`, deleted + with the adoption) implements the five RPCs; it is now supplied by the library + and mapped via `MapZbGalaxyRepository`. ## Configuration diff --git a/stillpending.md b/stillpending.md index c5d3f2f..1f51e91 100644 --- a/stillpending.md +++ b/stillpending.md @@ -133,8 +133,8 @@ No placeholder/empty/`Assert.True(true)` tests were found anywhere. - 📄 **7.1 D1 plan header stale** — `docs/plans/2026-06-14-deferred-followups.md:4` still says *"Plan only — NOT yet executed,"* but D1 is **done** (`Dashboard/DashboardSnapshotService.cs:198`, commit `4af24b9`). Update the plan status. - 📄 **7.2 `AlarmClientDiscovery.md` STA "production fix needed" prose is stale** — `docs/AlarmClientDiscovery.md:765-774` reads as a pending follow-up, but alarms now run through the worker STA / `GatewayAlarmMonitor` (merged). Re-check against current code. - 📄 **7.3 EventsHub "publisher side is a follow-up" comment is stale** — `Dashboard/Hubs/EventsHub.cs:9-17`; the `DashboardEventBroadcaster` exists, is DI-registered (`Dashboard/DashboardServiceCollectionExtensions.cs:47`), runs in the live loop (`Grpc/EventStreamService.cs:133`), and `SessionDetailsPage.razor` renders the feed. -- 📄 **7.4 CLAUDE.md project-name drift** — CLAUDE.md uses `src/MxGateway.Server`/`MxGateway.Tests`; the actual tree is `src/ZB.MOM.WW.MxGateway.*`. Misleads path-based work. -- ⚪ **7.5 Dead `MapSqlException` helper** — `Grpc/GalaxyRepositoryGrpcService.cs:350-360`, IDE0051-suppressed, kept for a hypothetical direct-SQL path that doesn't exist. +- ✅ **7.4 CLAUDE.md project-name drift — RESOLVED** (verified 2026-08-07): CLAUDE.md now uses the actual `src/ZB.MOM.WW.MxGateway.*` project paths throughout. +- ✅ **7.5 Dead `MapSqlException` helper — OBSOLETE** (noted 2026-08-07): `Grpc/GalaxyRepositoryGrpcService.cs` was deleted wholesale when the gateway adopted the shared `ZB.MOM.WW.GalaxyRepository` 0.2.0 package (2026-06-25, commit `8e196a7`), taking the dead helper with it. - **7.6 Accepted code-review gaps (`Won't Fix`, by design):** - `Client.Python-012` — `Session.invoke_raw` deliberately skips `ensure_mxaccess_success`, so an embedded MXAccess HRESULT failure surfaces silently (raw-parity inspection). `code-reviews/Client.Python/findings.md:290`. - `Contracts-003` — closed as not-a-defect. `code-reviews/Contracts/findings.md`.