Files
Joseph Doherty fe2a6db786
rust / build / test / clippy / fmt (push) Has been cancelled
Initial project state: .NET reference, design, Rust port (M0+M1), evidence
Layout:
- src/                    .NET 10 x64 reference: MxNativeCodec, MxNativeClient,
                          MxAsbClient, probes, tests, harnesses. Executable spec.
- design/                 Architectural plan for the Rust port (M0–M6), error
                          model, protocol invariants, risks (R1–R16), adversarial
                          review log (review.md).
- rust/                   Rust workspace. M0 skeleton + M1 codec parity.
                          mxaccess-codec: 215 unit tests + 2 cross-implementation
                          parity tests (byte-identical against .NET reference).
                          Other crates are M0 stubs awaiting M2+.
- captures/               Frida + netsh + pcap evidence per CLAUDE.md
                          ("captures are evidence, not throwaway logs").
- analysis/               Decompiled C# (frida/proxy/decompiled-*),
                          Ghidra exports for native DLLs (`exports/` only —
                          working state at `projects/` and AVEVA's input
                          binaries at `input/` are gitignored).
- docs/                   Reverse-engineering reference docs.
- tools/                  Setup-LiveProbeEnv.ps1 (Infisical credential fetcher),
                          Compute-Crc.ps1 (.NET parity helper).
- .github/workflows/      Rust CI: fmt + build + test + clippy on Windows.
- LICENSE                 MIT (Joseph Doherty, 2026).

Verified:
- cargo test --workspace → 217 passed (215 unit + 2 .NET parity), 0 failed
- cargo clippy --workspace -- -D warnings → clean
- cargo fmt --all -- --check → clean
- cargo publish --dry-run -p mxaccess-codec → packages cleanly

Excluded from history (see .gitignore):
- **/bin, **/obj, **/target — build artifacts
- analysis/ghidra/projects/ — Ghidra working state (regenerable)
- analysis/ghidra/input/ — AVEVA proprietary DLLs (vendor IP)

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

11 KiB

CLAUDE.md

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

Project goal

Build a native Rust interface that replaces the AVEVA/Wonderware MXAccess library, giving Rust applications an MXAccess-equivalent API for talking to the AVEVA System Platform. When COM bindings are needed, use microsoft/windows-rs.

Two-layer delivery target:

  1. Raw MXAccess replacement is key. The first deliverable is a faithful, byte-accurate Rust reimplementation of the MXAccess surface (register/advise/write/read/subscribe/callback dispatch) over the proven x64 transports. Parity with native MXAccess wire behavior comes before ergonomics — every operation must round-trip the same bytes and surface the same status as the captured native stack.
  2. High-level safe async via Tokio is the end goal. Above the raw layer, expose an idiomatic Rust API: async/await on Tokio, Send + Sync handles, typed errors instead of HRESULT, no unsafe in the public surface, structured subscription streams (e.g. Stream<Item = DataChange>), and cancellation via CancellationToken/drop. The raw layer should be usable on its own for power users; the Tokio layer is what most consumers should reach for.

The Rust port does not yet exist in this tree. What lives here today is the reverse-engineering reference: a working .NET 10 x64 managed implementation of the same protocol stack, plus the captures, decompiled artifacts, and wire-format docs that justify every byte of it. Treat the .NET code as the executable spec — do not invent protocol behavior; port what is already proven.

High-level architecture

The real MXAccess runtime is a 32-bit COM/native stack. The .NET assembly ArchestrA.MXAccess.dll is just an interop shim around LMXPROXYLib:

ArchestrA.MXAccess.dll  ->  LmxProxy.dll  ->  Lmx.dll / NmxAdptr.dll  ->  NmxSvc.exe

Two viable replacement paths have been proven from x64 (relevant to the Rust port):

  1. Managed DCOM/NDR directly to NmxSvc.exe — full MXAccess-compatible LMX/NMX behavior. Bypasses the x86-only NmxSvcps.dll proxy/stub by speaking ORPC + NTLM packet-integrity on the wire. This is what src/MxNativeClient does. The Rust port should follow the same approach using windows-rs for COM activation/RPCSS bootstrap and a hand-rolled DCE/RPC + NDR codec for the service interfaces.
  2. ASB (IASBIDataV2) over net.tcp to MxDataProvider — a higher-level data-service path that handles register/read/write/complete/subscribe without going through NMX at all. This is what src/MxAsbClient does. Useful as a regular-tag data-plane behind a compatibility router; does not cover callback-only MXAccess semantics.

docs/ASB-Native-Integration-Decision.md records the decision to use ASB as the preferred data-plane and reserve NMX for callback-only semantics that ASB cannot reach.

Layering inside the .NET reference

  • src/MxNativeCodec — pure protocol codec, no I/O. Handles MxReferenceHandle (the 20-byte LMX automation/attribute handle, including CRC-16/IBM signatures over lowercase UTF-16LE names), the NMX TransferData envelope, item-control (advise/unadvise) bodies, normal/secured write bodies, Write2 timestamped variants, subscription/status callback decoders (0x32, 0x33), reference-registration (0x10/0x11), and the MxStatus/MxValueKind/MxDataType model. This is the layer most directly equivalent to what the Rust port needs to reproduce first.
  • src/MxNativeClient — DCOM transport + session façade. Implements managed NTLMv2 (ManagedNtlmClientContext), DCE/RPC PDU framing (DceRpcPdu, DceRpcTcpClient), IObjectExporter::ResolveOxid, IRemUnknown::RemQueryInterface, the INmxService2 client (ManagedNmxService2Client), a managed callback exporter that serves INmxSvcCallback/IRemUnknown from a managed RPC endpoint, plus the high-level MxNativeSession and the MXAccess-shaped MxNativeCompatibilityServer. Talks SQL directly to the Galaxy Repository (GalaxyRepositoryTagResolver, GalaxyRepositoryUserResolver) instead of calling the x86 LMX resolver.
  • src/MxAsbClient — ASB/WCF data client (MxAsbDataClient) plus an MXAccess-shaped facade (MxAsbCompatibilityServer). Self-contained from NMX.
  • src/*.Probe — runnable diagnostic CLIs that exercise individual protocol slices end-to-end against the live local AVEVA install. They are how new behavior gets validated; their CLI flags double as documentation of the supported operations.
  • src/*.Tests — assertion-style runners (each is a console Program.cs, not a framework test project). Run them with dotnet run --project ....
  • src/MxTraceHarness (net481/x86) and src/NmxComHarness — instrumented harnesses that load the real x86 stack to capture ground-truth wire bytes.

Reverse-engineering artifacts (the spec, in priority order)

When in doubt about wire format, these are authoritative — read before guessing:

  • docs/MXAccess-Reverse-Engineering.md, docs/MXAccess-Public-API.md — overall map and the public surface to replicate.
  • docs/Loopback-Protocol-Findings.md, docs/NMX-COM-Contracts.md, docs/Transport-Correlation.md — DCE/RPC interfaces, opnums, and how Frida-observed native calls map to TCP frames.
  • docs/ASB-Variant-Wire-Format.md — ASB variant layout, scalar/array, timestamps, durations, quality/status.
  • docs/DotNet10-Native-Library-Plan.md — current parity matrix vs. ILMXProxyServer5, what's proven, what's blocked.
  • docs/MxNativeSession-API.md — the consumer-facing API surface the Rust port should converge on.
  • analysis/frida/write-body-matrix.tsv, write-array-body-matrix.tsv, write-mode-matrix.tsv — per-type captured write bodies.
  • analysis/proxy/nmxsvcps-procedures.tsv — decoded MIDL procedure table.
  • analysis/decompiled-mxaccess/, analysis/decompiled-interop/, analysis/decompiled/ — decompiled C# of the real assemblies. analysis/ghidra/ and analysis/native/ for native code.
  • captures/0NN-frida-* — controlled Frida captures, one per write/subscribe scenario. frida-to-tcp-map.tsv inside a capture correlates native calls to the wire.
  • work_remain.md — current status of what's done vs. open across both managed paths.

Common commands

All .NET projects target net10.0-windows / x64 (except the x86 MxTraceHarness, which is net481).

# Build the protocol layers
dotnet build src\MxNativeCodec\MxNativeCodec.csproj
dotnet build src\MxNativeClient\MxNativeClient.csproj
dotnet build src\MxAsbClient\MxAsbClient.csproj

# Run the assertion-style test programs
dotnet run --project src\MxNativeCodec.Tests\MxNativeCodec.Tests.csproj
dotnet run --project src\MxNativeClient.Tests\MxNativeClient.Tests.csproj
dotnet run --project src\MxAsbClient.Tests\MxAsbClient.Tests.csproj

# Live probes against the local AVEVA install (require NTLM env + Galaxy SQL access)
dotnet run --project src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-write --tag=TestChildObject.TestInt --value=123 --objref-only
dotnet run --project src\MxNativeClient.Probe\MxNativeClient.Probe.csproj -c Release -- --probe-session-subscribe --tag=TestChildObject.TestInt --subscribe-hold-seconds=5 --objref-only

# Populate live-probe env vars from Infisical (via wwtools/secrets/Get-Secret.ps1).
# Sets MX_LIVE, MX_NMX_HOST, MX_GALAXY_DB, MX_TEST_USER, MX_TEST_DOMAIN,
# MX_TEST_PASSWORD, and (when configured) MX_ASB_SHARED_SECRET. Dot-source so
# the env vars persist in the calling session. Never inline plaintext.
. .\tools\Setup-LiveProbeEnv.ps1
. .\tools\Setup-LiveProbeEnv.ps1 -SkipAsbSecret    # if ASB secret not yet in Infisical
.\tools\Setup-LiveProbeEnv.ps1 -DryRun             # print what would be set

There is no solution file or workspace runner — build/test each project explicitly. There is no lint config; the projects use Nullable and ImplicitUsings enabled and rely on the compiler.

Rust workspace (M0 onwards)

The Rust port lives under rust/ (sibling of src/). Toolchain pinned to 1.85 stable via rust/rust-toolchain.toml; edition 2024; license MIT.

# All commands run from the rust/ workspace root
cd rust

# Workspace-wide
cargo build --workspace --all-targets
cargo test --workspace --no-fail-fast
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all -- --check

# Single crate
cargo build -p mxaccess-codec
cargo test  -p mxaccess-codec

# Examples (live under crates/mxaccess/examples/, invoke with -p mxaccess)
cargo run -p mxaccess --example connect-write-read
cargo run -p mxaccess --example subscribe -- --tag TestChildObject.TestInt
cargo run -p mxaccess --example asb-subscribe -- --tag TestChildObject.TestInt

# Live integration tests (require AVEVA + Galaxy DB; gated by MX_LIVE)
. ..\tools\Setup-LiveProbeEnv.ps1                # dot-source — fetches creds from Infisical
cargo test -p mxaccess --features live -- --ignored

# Publish-check (M0 DoD)
cargo publish --dry-run -p mxaccess-codec

CI: .github/workflows/rust.yml runs fmt --check, build, test, clippy -D warnings on Windows.

Working rules specific to this project

  • Do not fabricate protocol behavior. Every wire shape in the .NET reference is backed by a Frida capture, decompiled source, or live probe. When extending the Rust port, point to the same evidence — capture a new fixture if one doesn't exist.
  • Preserve unknown bytes. The codecs in MxNativeCodec deliberately round-trip unknown tag/session/quality fields rather than zeroing them. Mirror this in Rust; consumers depend on byte-for-byte parity with native MXAccess for some flows.
  • x64 only for the replacement. The whole point is to escape the 32-bit NmxSvcps.dll proxy/stub. Do not introduce a dependency on the x86 stack from any replacement code; if x86 is unavoidable for capture/validation, isolate it in MxTraceHarness/NmxComHarness.
  • COM in Rust → use windows-rs. For COM activation, IUnknown, OBJREF marshaling, and registered type-library callback paths, prefer windows/windows-rs types over hand-rolled FFI.
  • Galaxy Repository access is direct SQL (see GalaxyRepositoryTagResolver.cs), not LMX. The Rust port should do the same — query the dbo.gobject / dbo.instance / dbo.dynamic_attribute / dbo.attribute_definition / dbo.primitive_instance / dbo.package tables, then walk the package-inheritance chain via package.derived_from_package_id (the recursive CTE in GalaxyRepositoryTagResolver.cs:209-293, named deployed_package_chain). Combine with CRC-16/IBM signature computation locally to reproduce the native handle without touching x86 LMX. Verified against wwtools/grdb/ (the sibling Galaxy SQL exploration repo) and wwtools/grdb/queries/attributes.sql. Resolver input is tag_name-form (e.g. DelmiaReceiver_001), NOT contained_name-form (e.g. TestMachine_001.DelmiaReceiver) — the two are asymmetric in the schema. Earlier drafts of this file listed aa_attribute / aa_object / mx_attribute_category as the surface; those names do not exist as tables in the Galaxy DB and have been corrected.
  • Captures and probe outputs in captures/, analysis/frida/, analysis/proxy/ are evidence, not throwaway logs. Don't delete or rewrite them when refactoring.