Files
mxaccessgw/CLAUDE.md
T
Joseph Doherty d692232191 dashboard: clear deferred items — EventsHub publisher + doc refresh
EventsHub publisher (closes the v2.1 follow-up flagged in the previous commit)

EventStreamService now mirrors every MxEvent it forwards to a gRPC client
into the `EventsHub` group for the session. The fan-out goes through a new
singleton `IDashboardEventBroadcaster`:

  * IDashboardEventBroadcaster — abstraction so EventStreamService doesn't
    take a direct dependency on SignalR.
  * DashboardEventBroadcaster — singleton implementation that hands the
    SendAsync to IHubContext<EventsHub> as fire-and-forget. Errors are
    logged at debug and dropped so the source gRPC stream is never
    blocked.

EventStreamService now takes IDashboardEventBroadcaster as a ctor parameter
and calls Publish(sessionId, publicEvent) once per event after sequence
filtering, before the bounded queue write. Test fixtures and the live
integration harness pass NullDashboardEventBroadcaster.Instance so the
broadcaster is a no-op in unit tests.

SessionDetailsPage adds a "Recent events" panel:
  * implements IAsyncDisposable
  * opens a second HubConnection via DashboardHubConnectionFactory targeting
    /hubs/events
  * calls SubscribeSession(SessionId) on Start
  * renders the most recent 50 events in a small table (worker seq, family,
    server/item handle, alarm reference when the event is OnAlarmTransition)
  * shows a live/offline conn-pill driven by HubConnection.Closed /
    Reconnected events

The dashboard mirror is intentionally passive — events appear only while a
gRPC client is also consuming that session's events. Documented as such in
the empty-state copy and in GatewayDashboardDesign.md.

Documentation refresh

Every doc that referenced the retired options (PathBase, RequireAdminScope,
RequiredGroup) and the old API-key-cookie auth flow is updated to describe
the new model:

  * CLAUDE.md — Authentication section now explains LDAP bind +
    GroupToRole + HubToken bearer flow.
  * gateway.md — Dashboard section: root-mounted routes, snapshot/alarms/
    events SignalR hubs, LDAP cookie + bearer scheme.
  * docs/GatewayConfiguration.md — drop PathBase / RequireAdminScope rows,
    add GroupToRole row, append "Authorization policies" and "SignalR hubs"
    subsections describing the three policies and the /hubs/* endpoints.
  * docs/GatewayDashboardDesign.md — hosting model (root mount, new
    endpoint layout), Realtime Updates rewritten as a hub table
    (DashboardSnapshotHub / AlarmsHub / EventsHub with producers, payloads,
    and routing), Authentication And Authorization rewritten around LDAP +
    role mapping + the hub bearer flow, Configuration block updated.
  * docs/GatewayProcessDesign.md — security-section dashboard paragraph
    and the example config block both refreshed to LDAP/role auth.
  * docs/ImplementationPlanGateway.md — dashboard-auth deliverable list
    updated (LDAP bind + GroupToRole + /hubs/token bearer mint replace the
    API-key login flow).
  * docs/GatewayTesting.md — DashboardLdapLiveTests blurb describes the
    GroupToRole fixture (`{ GwAdmin: Admin }`) instead of the retired
    RequiredGroup default; success-path assertion explains the role-claim
    check.

Verification: 475 server tests, 275 worker tests (+ 9 dev-rig skips), 18
integration tests (live MxAccess + LDAP + Galaxy) all pass — including the
live worker smoke test fixture that now constructs EventStreamService with
the new broadcaster parameter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 02:07:30 -04:00

11 KiB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Project Overview

mxaccessgw is the MXAccess Gateway: a gRPC service that gives modern (.NET, Go, Rust, Python, Java) clients full MXAccess parity without forcing them to load 32-bit MXAccess COM, run x86, or own an STA message pump.

The architecture is a two-process design — read gateway.md before making structural changes:

  • Gateway (src/MxGateway.Server, .NET 10, x64): ASP.NET Core gRPC server. Owns the public API, sessions, auth, the Blazor dashboard, and the Galaxy Repository SQL browse RPCs. Never instantiates MXAccess COM directly.
  • Worker (src/MxGateway.Worker, .NET Framework 4.8, x86): one process per session. Owns one MXAccess COM instance on a dedicated STA, pumps Windows messages, and converts COM events to protobuf.
  • IPC: gateway↔worker uses one bidirectional named pipe per worker (mxaccess-gateway-{gatewayPid}-{sessionId}) with length-prefixed WorkerEnvelope protobuf frames. Gateway hosts the pipe server and launches the worker. gRPC is not used inside the worker — .NET Framework 4.8 doesn't have a first-class gRPC stack.
  • Contracts (src/MxGateway.Contracts): multi-targets net10.0;net48 and owns the .proto files (mxaccess_gateway.proto, mxaccess_worker.proto, galaxy_repository.proto). All other projects consume the generated types from here. Do not hand-edit anything under Generated/.

The worker must do all MXAccess COM calls on its dedicated STA thread, and the STA loop must pump Windows messages (MsgWaitForMultipleObjectsEx + PeekMessage/DispatchMessage) so MXAccess events deliver. A plain blocking queue on an STA is not enough.

Build, Test, Run

# Full solution build (gateway, worker, contracts, tests)
dotnet build src/MxGateway.sln

# Worker must be built x86 — the gateway looks for MxGateway.Worker.exe under bin\x86
dotnet build src/MxGateway.Worker/MxGateway.Worker.csproj -p:Platform=x86

# Gateway tests (no MXAccess required — uses FakeWorkerHarness)
dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj
dotnet test src/MxGateway.Worker.Tests/MxGateway.Worker.Tests.csproj -p:Platform=x86

# Run gateway locally (defaults bound under MxGateway:* in src/MxGateway.Server/appsettings.json)
dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj

# API-key admin CLI (same exe, "apikey" subcommand)
dotnet run --project src/MxGateway.Server/MxGateway.Server.csproj -- apikey create --display-name "dev" --scopes session,invoke,event,metadata,admin

Single test by name (xUnit --filter):

dotnet test src/MxGateway.Tests/MxGateway.Tests.csproj --filter FullyQualifiedName~GatewayEndToEndFakeWorkerSmokeTests

Live MXAccess integration tests are opt-in because they need installed MXAccess COM and live provider state:

$env:MXGATEWAY_RUN_LIVE_MXACCESS_TESTS = "1"
dotnet test src/MxGateway.IntegrationTests/MxGateway.IntegrationTests.csproj --filter FullyQualifiedName~WorkerLiveMxAccessSmokeTests

Live LDAP tests use MXGATEWAY_RUN_LIVE_LDAP_TESTS=1. See docs/GatewayTesting.md for the full opt-in matrix and LiveMxAccessFactAttribute / LiveLdapFactAttribute for the gating logic.

Clients

Each language client is in clients/<lang>/ with its own README. They all consume the shared .proto files in src/MxGateway.Contracts/Protos:

  • clients/dotnet: dotnet build clients/dotnet/MxGateway.Client.sln
  • clients/python: python -m pip install -e ".[dev]"; python -m pytest
  • clients/rust: cargo test --workspace; cargo clippy --workspace --all-targets -- -D warnings
  • clients/java: gradle test (Java 21)
  • Go client lives alongside as mxgw-go in the cross-language matrix

End-to-end matrix runner (needs running gateway + worker + valid API key):

$env:MXGATEWAY_API_KEY = "<api-key>"
powershell -ExecutionPolicy Bypass -File scripts/run-client-e2e-tests.ps1

Repository-Specific Conventions

  • Build properties (src/Directory.Build.props) enforce Nullable=enable, TreatWarningsAsErrors=true, latest analyzers, and EnforceCodeStyleInBuild=true. New warnings break the build — fix them, don't suppress unless the suppression has a narrow reason.
  • Style guides in docs/style-guides/ are authoritative. Follow CSharpStyleGuide.md for gateway/worker/.NET-client code: file-scoped namespaces, sealed by default, Async suffix on Task-returning methods, MXAccess-aligned names (MxStatusProxy, ServerHandle, ItemHandle, HResult).
  • MXAccess parity is the contract. Don't "fix" surprising MXAccess behavior (e.g., WriteSecured failing before a value-bearing NMX body, distinct OperationComplete semantics, invalid-handle exceptions) unless the client explicitly opts into a non-parity mode. The installed MXAccess COM component is the baseline.
  • Don't synthesize events. The gateway forwards only events the worker emits; it never invents OperationComplete from write completion or command replies.
  • One worker per session, one event subscriber per session (v1). Multi-subscriber fan-out and reconnectable sessions are explicitly out of scope — see docs/DesignDecisions.md.
  • Gateway restart does not reattach orphan workers. The first version terminates orphaned workers on startup; do not design code paths that assume reattachment.
  • No Blazor UI component libraries. Dashboard uses local Bootstrap CSS/JS only — do not introduce MudBlazor, Radzen, FluentUI, etc.
  • Don't log secrets or full tag values by default. API keys, passwords, WriteSecured payloads, and AuthenticateUser credentials must never reach logs. Value logging is opt-in and redacted.
  • Generated code under src/MxGateway.Contracts/Generated/, clients/*/generated*/, clients/python/src/mxgateway/generated/, etc., is build output. Don't hand-edit. To regenerate, build the contracts project (dotnet build src/MxGateway.Contracts/MxGateway.Contracts.csproj) or run the per-client generation step in that client's README.
  • Documentation style (StyleGuide.md): PascalCase filenames, no marketing language, present tense, explain why not what.
  • Update docs in the same change as the source. When public APIs, contracts, configuration, build steps, security behavior, event shapes, value conversion, status mapping, or lifecycle rules change, the affected docs (gateway.md, docs/, client READMEs, design docs) must change in the same commit. Don't leave stale prose describing old behavior.

Source Update Workflow

When source code changes, build and test the affected component before reporting work done. If the change crosses component boundaries, build each affected component — don't rely on a single top-level build:

Changed area Required verification
Contracts or .proto files regenerate generated code, then build gateway, worker, and every generated client touched by the contract
Gateway server, sessions, workers, gRPC, dashboard, metrics dotnet build src/MxGateway.Server and run affected gateway / fake-worker tests
Worker IPC, STA, MXAccess, conversion dotnet build src/MxGateway.Worker -p:Platform=x86 and run worker tests
.NET client dotnet build clients/dotnet/MxGateway.Client.sln and run its tests
Go client gofmt, go build ./..., go test ./... from clients/go
Rust client cargo fmt, cargo check --workspace, cargo test --workspace, cargo clippy --all-targets -- -D warnings from clients/rust
Python client python -m pytest from clients/python
Java client gradle test from clients/java
Integration tests run only when MXAccess COM, provider state, and external services are available; otherwise document why skipped

Design Sources To Consult Before Non-Trivial Changes

  • gateway.md — top-level architecture, command/event surface, IPC envelope, STA thread model, fault handling.
  • glauth.md — local LDAP server (GLAuth on localhost:3893, base DN dc=lmxopcua,dc=local) used for dev authn. Pre-provisioned users (admin/admin123, readonly/readonly123, etc.) and the role→capability mapping live there.
  • docs/DesignDecisions.md — v1 choices (MXAccess COM target LMXProxyServerClass from C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll, API-key-in-SQLite auth, fail-fast event backpressure, etc.).
  • docs/GatewayProcessDesign.md, docs/MxAccessWorkerInstanceDesign.md, docs/WorkerFrameProtocol.md, docs/WorkerProcessLauncher.md — detailed component designs.
  • docs/GatewayConfiguration.md — full MxGateway:* options bound by GatewayOptions and validated at startup by GatewayOptionsValidator.
  • docs/GatewayTesting.md — fake worker harness, live MXAccess smoke, parity matrix, cross-language smoke matrix.
  • docs/ToolchainLinks.md — installed compiler/SDK paths on this dev box (.NET 10.0.201, Go 1.26.2, Rust 1.95, Python 3.12.10, Temurin 21, protoc 34.1, etc.).

External analysis sources referenced by design docs:

  • C:\Users\dohertj2\Desktop\mxaccess — MXAccess analysis project. Key files: docs/MXAccess-Public-API.md (COM class, ProgID, CLSID, method list, event signatures, MxDataType, MxStatus, MXSTATUS_PROXY), docs/MXAccess-Reverse-Engineering.md (installed runtime path, x86 COM constraints), docs/Current-Sprint-State.md (parity gaps), src/MxTraceHarness/ (x86 harness using the real COM interop), captures/ and analysis/ (observed native behavior).
  • C:\Users\dohertj2\Desktop\lmxopcua\gr — Galaxy Repository (ZB SQL DB) notes. Key files: connectioninfo.md, layout.md, schema.md, queries/hierarchy.sql, queries/attributes.sql, queries/attributes_extended.sql, queries/change_detection.sql. Connection is SQL Server localhost, database ZB, Windows Auth.

Authentication

Gateway gRPC clients authenticate with an API key in metadata: authorization: Bearer mxgw_<key-id>_<secret>. Keys are stored hashed (with a peppered SHA) in a gateway-owned SQLite DB (default C:\ProgramData\MxGateway\gateway-auth.db). Scopes (session, invoke, event, metadata, admin) gate specific RPCs; missing → Unauthenticated, insufficient → PermissionDenied. The apikey subcommand on the server exe manages keys; see src/MxGateway.Server/Security/Authentication/.

Dashboard auth is LDAP-backed (separate from the gRPC API-key model). /login binds against MxGateway:Ldap and maps the user's LDAP groups to Admin or Viewer via MxGateway:Dashboard:GroupToRole, then issues an HTTP-only secure __Host-MxGatewayDashboard cookie. SignalR hubs at /hubs/{snapshot,alarms,events} accept either the cookie or a 30-minute bearer minted at /hubs/token. Dashboard:AllowAnonymousLocalhost bypasses auth on loopback when enabled.

Process / Platform Notes

  • Working tree is on Windows (C:\Users\dohertj2\Desktop\mxaccessgw). PowerShell is the native shell for tooling commands; bash is fine for git/grep/find.
  • The worker reference to ArchestrA.MXAccess.dll uses an absolute HintPath to C:\Program Files (x86)\ArchestrA\Framework\Bin\ArchestrA.MXAccess.dll. The worker only builds where MXAccess is installed (this dev box).
  • The repo is not a git repository at the top level — there's no .git directory in the working tree.