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

126 lines
11 KiB
Markdown

# 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](https://github.com/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).
```powershell
# 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.
```powershell
# 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.