Mark A2 handoff and stillpending §2 adopted; note the host-side design (GatewayBrowseScopeProvider, dashboard summary projector), the lib 0.2.0 upstream changes, and caveats (NSSM deploy config, pre-existing NU1903 + IntegrationTests EventStreamService breaks). Point CLAUDE.md at the package.
15 KiB
A2 — Adopt the shared ZB.MOM.WW.GalaxyRepository library
✅ ADOPTED 2026-06-25 (branch
feat/galaxyrepository-adoption). mxaccessgw now consumesZB.MOM.WW.GalaxyRepository0.2.0 and the inline Galaxy code is deleted (27 files, −2959 LOC). What shipped:
- Lib 0.2.0 (published to Gitea) closed the two real upstream gaps: alarm-attribute discovery (
GalaxyAlarmAttributeRow+IGalaxyRepository.GetAlarmAttributesAsync+AlarmAttributesSql) and an injectableIGalaxyBrowseScopeProvider(defaultNullGalaxyBrowseScopeProvider= no scoping; HistorianGateway @ 0.1.0 unaffected) wired into the lib's gRPC service. Lib test suite: 64 green.- mxaccessgw registers
GatewayBrowseScopeProvider : IGalaxyBrowseScopeProvider(reads the API key'sEffectiveConstraints.BrowseSubtrees) beforeAddZbGalaxyRepository(config,"MxGateway:Galaxy"), mapsMapZbGalaxyRepository(), and switchedGatewayGrpcScopeResolvergalaxy request types to the lib.Grpcnamespace (the global authz interceptor now intercepts the lib service). The dashboard summary stays host-side (DashboardGalaxySummaryProjector, memoized by cacheSequence) since the lib entry doesn't carry it. Server build zero-warning; 327 targeted tests green.- The end-to-end host authz chain is covered (
GalaxyRepositoryHostWiringTests), and the lib gained the ported browse-projector / deploy-notifier / refresh-service (Server-005 timeout guard) tests.Caveats / follow-ups (see "Post-adoption notes" at the bottom). Original handoff (now historical) follows.
Handoff note. Written 2026-06-25 from the HistorianGateway side, where the shared lib is already consumed in production. This is the mxaccessgw half of the cross-repo "Galaxy-browse normalization" follow-on (HistorianGateway
pending.mdA2 / scadaproj component-normalization).
Goal
Replace mxaccessgw's inline Galaxy-browse code (src/ZB.MOM.WW.MxGateway.Server/Galaxy/**
plus Grpc/GalaxyRepositoryGrpcService.cs + Grpc/GalaxyProtoMapper.cs) with a
PackageReference to the shared ZB.MOM.WW.GalaxyRepository library, so there is one
Galaxy-browse implementation shared by both sidecars instead of two copies that can drift.
The wire contract is unchanged — both repos already serve the same galaxy_repository.v1
proto — so no client change is required. This is an internal refactor.
Why this is safe to do now
The shared lib was extracted from this very code: the file names under
src/ZB.MOM.WW.MxGateway.Server/Galaxy/** map ~1:1 onto
scadaproj/ZB.MOM.WW.GalaxyRepository/src/ZB.MOM.WW.GalaxyRepository/**. HistorianGateway
consumes it (PackageReference @ 0.1.0) and runs it live against wonder-sql-vd03. So the SQL
provider, hierarchy cache, deploy notifier, snapshot store, background refresh service, projectors,
glob matcher, tag lookup, and the gRPC service are already proven in the package — the work here
is wiring + reconciling the mxaccessgw-only pieces, not reimplementing browse.
The clean part — 1:1 swap
These inline files are superseded by the package (delete on adoption; rebind references to namespace
ZB.MOM.WW.GalaxyRepository):
mxaccessgw inline (…Server/Galaxy/) |
Shared lib equivalent |
|---|---|
GalaxyRepository.cs / IGalaxyRepository.cs |
same |
GalaxyHierarchyCache.cs / IGalaxyHierarchyCache.cs / GalaxyHierarchyCacheEntry.cs |
same |
GalaxyHierarchyRefreshService.cs |
same (registered as HostedService by the lib's DI ext) |
GalaxyDeployNotifier.cs / IGalaxyDeployNotifier.cs / GalaxyDeployEventInfo.cs |
same |
GalaxyHierarchySnapshot*.cs / IGalaxyHierarchySnapshotStore.cs |
same |
GalaxyHierarchyProjector.cs / GalaxyBrowseProjector.cs / GalaxyHierarchyIndex.cs |
same |
GalaxyObjectView.cs / GalaxyHierarchyRow.cs / GalaxyAttributeRow.cs |
same |
GalaxyBrowseChildrenResult.cs / GalaxyHierarchyQueryResult.cs |
same |
GalaxyTagLookup.cs / GalaxyGlobMatcher.cs / GalaxyCacheStatus.cs |
same |
GalaxyRepositoryOptions.cs |
GalaxyRepositoryOptions (lib ships no validator — see below) |
Grpc/GalaxyRepositoryGrpcService.cs / Grpc/GalaxyProtoMapper.cs |
lib bundles its own Grpc/GalaxyRepositoryGrpcService.cs + GalaxyProtoMapper.cs, mapped by MapZbGalaxyRepository() — but verify authz parity first, see ⚠️ below |
Galaxy/GalaxyRepositoryServiceCollectionExtensions.cs |
replaced by the lib's AddZbGalaxyRepository / MapZbGalaxyRepository |
⚠️ The hard part — mxaccessgw-only consumers the shared lib does NOT cover
HistorianGateway was a greenfield consumer (browse + dashboard tree only). mxaccessgw has extra consumers of the Galaxy cache/hierarchy that the shared lib was not extracted with. These are the real work and the real risk:
-
Alarm watch-list / alarm-attribute discovery — capability GAP in the lib.
- mxaccessgw has
Galaxy/GalaxyAlarmAttributeRow.csandGalaxyRepository.GetAlarmAttributesAsync(), consumed byAlarms/AlarmWatchListResolver.cs(+IAlarmWatchListResolver,GatewayAlarmMonitor). - The shared lib has
GalaxyAttributeRow+GetAttributesAsync()only — there is noGalaxyAlarmAttributeRow/GetAlarmAttributesAsyncin0.1.0. - Decision needed: either (a) push the alarm-attributes SQL projection upstream into the
shared lib (a
0.2.0additive feature — preferred, keeps it shared) and re-consume, or (b) keepAlarmWatchListResolver+ the alarm-attributes query inline in mxaccessgw, layered on top of the sharedIGalaxyRepository/IGalaxyHierarchyCache. (a) is the spirit of A2.
- mxaccessgw has
-
Dashboard consumers —
Dashboard/DashboardGalaxyProjector.cs,DashboardBrowseService.cs(+IDashboardBrowseService,DashboardBrowseModel),DashboardSnapshotService.cs,DashboardGalaxySummary.cs, and the Razor pagesGalaxyPage.razor/DashboardHome.razorallusing ZB.MOM.WW.MxGateway.Server.Galaxy. Mechanical rebind toZB.MOM.WW.GalaxyRepository, but confirm every type they touch ispublicin the lib (most are; double-check projector/snapshot types). -
Security / authorization —
Security/Authorization/ConstraintEnforcer.csandGatewayGrpcScopeResolver.csconsume Galaxy hierarchy to enforce per-key constraints. Rebind, and see the gRPC-service question below.
⚠️ gRPC-service authz parity — VERIFIED 2026-06-25: NOT safe to swap wholesale. The authorization is split across two layers, and the part that matters is baked into the service body:
- Authentication + scope gating IS interceptor-based ✅ (safe).
Security/Authorization/GatewayGrpcAuthorizationInterceptor.csauthenticates the API key and enforces the required scope; all five Galaxy RPCs map to themetadatascope (GatewayGrpcScopeResolver.cs:23-27→MetadataRead). The interceptor also pushes the identity into an ambientIGatewayRequestIdentityAccessorfor the call. This mirrors HistorianGateway and is shared-lib compatible. - Per-key browse-subtree CONSTRAINT FILTERING is baked into the service body ❌ (the blocker).
mxaccessgw's
Grpc/GalaxyRepositoryGrpcService.csinjectsIGatewayRequestIdentityAccessorandResolveBrowseSubtrees()(:287-291) readsidentityAccessor.Current.EffectiveConstraints.BrowseSubtrees, threading those globs into the projectors forDiscoverHierarchy(:78),BrowseChildren(:138), and theWatchDeployEventscount scoping (:180,196,230-237) — i.e. it restricts which Galaxy objects each API key can see. The shared lib's service injects no identity accessor and passesbrowseSubtreeGlobs: nulleverywhere; its own XML doc states it "applies no per-identity browse-subtree filtering … Authorization (including any subtree scoping) is the responsibility of the hosting gateway's interceptor layer."
Severity — silent per-key data exposure. Glob semantics (GalaxyHierarchyProjector.cs:225-227):
null/empty → match everything; non-empty → restrict. So keys with no browse constraint behave
identically in both services (no regression), but a key with a BrowseSubtrees constraint would, on
the shared service, receive the entire Galaxy hierarchy — a metadata-scoped key silently bypassing
its restriction. The boundary is locked by tests that would fail against the shared service:
Tests/Gateway/Grpc/GalaxyRepositoryGrpcServiceTests.cs → BrowseChildren_BrowseSubtreesConstraint_FiltersChildren
(:437) and DiscoverHierarchy_WithSubtreeRootAndDepth_FiltersDescendants (:90).
Recommendation. Do not call MapZbGalaxyRepository() directly. The shared projectors already
accept a browseSubtreeGlobs param, so either fix is small plumbing:
- Preferred (spirit of A2 — push upstream): give the shared
GalaxyRepositoryGrpcServicean optional browse-subtree-constraint provider (e.g.IBrowseSubtreeConstraintProvider/delegate returning globs —nullin HistorianGateway, identity-derived in mxaccessgw). One shared service, both behaviors; bundle with the same0.2.0bump as the alarm-attributes gap above. - Fallback (isolate to mxaccessgw): keep a thin mxaccessgw service that resolves subtrees from
identity and calls the shared (public) projectors, and map that instead of
MapZbGalaxyRepository().
Concrete wiring steps (template from the HistorianGateway side)
-
nuget.config— mxaccessgw'spackageSourceMappingfor thedohertj2-giteasource currently listsZB.MOM.WW.*up toThemebut NOT GalaxyRepository. Add:<package pattern="ZB.MOM.WW.GalaxyRepository" />under the
dohertj2-gitea<packageSource>. (Easy to miss — restore will fail without it.) -
Server
.csproj— add:<PackageReference Include="ZB.MOM.WW.GalaxyRepository" Version="0.1.0" />(or whatever version ships the alarm-attributes add, if you go route 1(a) above.)
-
DI + endpoint wiring (in mxaccessgw's host composition —
GatewayApplication.cs/ itsProgram), mirroring HistorianGateway'sProgram.cs:using ZB.MOM.WW.GalaxyRepository; using ZB.MOM.WW.GalaxyRepository.DependencyInjection; // service registration — bind from mxaccessgw's section path (NOT top-level "Galaxy"): builder.Services.AddZbGalaxyRepository(builder.Configuration, "MxGateway:Galaxy"); // confirm the exact section path mxaccessgw uses today // endpoint pipeline (after AddGrpc): app.MapZbGalaxyRepository();Delete mxaccessgw's own
Galaxy/GalaxyRepositoryServiceCollectionExtensions.csregistrations. -
Option validation — the shared lib binds only, ships no validator (deliberate). mxaccessgw already validates Galaxy options via
Configuration/GatewayOptionsValidator.cs— keep that; it stays the owner of fail-fast validation, exactly as HistorianGateway'sConfigPreflightdoes. -
Health check — keep mxaccessgw's existing Galaxy-SQL readiness check; read the connection string from the same
MxGateway:Galaxysection the lib binds (HistorianGateway does this with a rawSELECT 1SqlConnectionHealthCheck— seeProgram.cs:196).
Tests
mxaccessgw has a large Tests/Galaxy/** suite that duplicates the shared lib's own tests
(GalaxyHierarchyCacheTests, GalaxyHierarchyProjectorTests, GalaxyHierarchySnapshotStoreTests,
GalaxyProtoMapperTests, GalaxyHierarchyIndexTests). On adoption these are covered upstream and can
be deleted. Keep the mxaccessgw-specific ones that exercise behavior the lib doesn't own:
GalaxyAlarmAttributeMappingTests, GalaxyFilterInputSafetyTests, GalaxyRepositoryGrpcServiceTests
(unless their subjects move upstream too), and the live IntegrationTests/Galaxy/**.
Suggested order — all DONE (2026-06-25)
Verify the gRPC-service authz parity questionDONE: wholesale swap was unsafe — per-key browse-subtree filtering was baked into the service body. Resolved by pushing an injectableIGalaxyBrowseScopeProviderhook upstream into the lib (option preferred in the ⚠️ block).Decide alarm-attributesDONE: upstreamed into the lib as part of0.2.0.Upstream + publish + bumpDONE: lib0.1.0 → 0.2.0, packed and pushed to the Gitea feed (verified live).DONE.nuget.config+csproj+ DI/endpoint wiring; delete inline; rebindDelete duplicated tests; build zero-warning; run the suiteDONE (lib 64 green; gateway 327 targeted green). Live-validate browse + alarm watch-list is the one remaining manual step (needs Galaxy SQL + a running gateway — opt-inMXGATEWAY_RUN_LIVE_GALAXY_TESTS=1); not runnable from the dev Mac.- Remaining: propagate to the scadaproj umbrella index + HistorianGateway's
pending.md§A2 (mark adopted) — cross-repo, do in those repos.
Post-adoption notes / caveats
- Deployment config (NSSM): the deployed services (
MxAccessGwon 10.100.0.48; the wonder host) read config from NSSM environment variables, notappsettings.json. The lib'sSnapshotCachePathdefault is empty (persistence no-ops).appsettings.jsonsetsMxGateway:Galaxy:SnapshotCachePath+PersistSnapshot, but the deployments must carryMxGateway__Galaxy__SnapshotCachePathandMxGateway__Galaxy__PersistSnapshotin 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_sqlite32.1.11 advisory (GHSA-2m69-gcr7-jv3q, no upstream patch) that breaks the build underTreatWarningsAsErrors— already red onmain. Resolved with a targetedNuGetAuditSuppressinsrc/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.csconstructsEventStreamServicewith 6 ctor args, but a prior event-stream refactor reduced that ctor — so the IntegrationTests project does not compile (already broken onmain, 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. - 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.
Reference pointers
- Working consumer (copy this):
~/Desktop/HistorianGateway/src/ZB.MOM.WW.HistorianGateway.Server/Program.cs(AddZbGalaxyRepository@ ~line 174,MapZbGalaxyRepository@ ~line 314) + itsnuget.config+ Server.csproj. - Shared lib source:
~/Desktop/scadaproj/ZB.MOM.WW.GalaxyRepository/(DI ext atsrc/ZB.MOM.WW.GalaxyRepository/DependencyInjection/GalaxyRepositoryServiceCollectionExtensions.cs). - Tracked in: HistorianGateway
pending.md§A2 +CLAUDE.md→ Known Follow-Ons; scadaproj component normalization.