Files
mxaccess/CHANGELOG.md
Joseph Doherty 8b50c0fd43 CHANGELOG: curate post-F43 work into V1 entry
The CHANGELOG was cut at F43 and didn't reflect the work that landed
afterwards on the same V1 milestone. Update the V1 [Unreleased] entry
to cover:

Added (since F43):
- F45 — recovery replay re-issues RegisterReference for buffered subs
- F47 — unsubscribe skips UnAdvise for buffered subs
- F49 / F50 / F51 — live verification + Suspend/Activate captures +
  ASB type-matrix expansion with new fixture round-trip tests
- F52.{1,2,3} — codec performance optimisations (BytesMut output,
  thread-local name-signature cache, caller-supplied scratch buffer)
- F54 — per-operation correlation + compat OnWriteComplete fan-out
- F55 — DCOM-managed INmxSvcCallback sink (Path A)
- F56 — Connect/AddSubscriberEngine round-trip in subscribe path
- MxStatus synthesizer kernel ported (settles R3/R4)

Known limitations (post-resolution):
- Drop F45 / F46 / R3+R4 — all resolved.
- Add F53 protocol-crate missing-docs deferral.
- F3 entry now links the new docs/F3-cross-domain-ntlm-recipe.md.

Publish-order section keeps the DAG but flags F48 (no crates.io
publish) up front so anyone reading the recipe knows it's hygiene
not release prep.

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

179 lines
9.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Changelog
All notable changes to the `mxaccess` workspace are documented here. The
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/);
the workspace as a whole follows [SemVer](https://semver.org/) but the
0.0.x line is pre-release / API-unstable.
## [Unreleased] — V1 — 2026-05-07
V1 is the first publishable cut. Closes M0 → M6 from
`design/60-roadmap.md`. The workspace stays at `version = "0.0.0"`
indefinitely (F48 — internal usage only, no crates.io publish; consumers
depend via path or git).
### Added
- **`mxaccess-codec`** — pure protocol codec covering `MxReferenceHandle`,
`NmxTransferEnvelope`, `NmxItemControlMessage`, `NmxWriteMessage` (scalar
+ array, normal + timestamped), `NmxSecuredWrite2Message`,
`NmxSubscriptionMessage` (single + multi-record DataUpdate per F44),
`NmxReferenceRegistrationMessage`, `NmxMetadataQueryMessage`,
`NmxOperationStatusMessage`, `ObservedWriteBodyTemplate`, ASB Variant +
`AsbStatus` + `RuntimeValue`, `MxStatus`, `MxValueKind`, `MxDataType`,
`MxValue`. Counting-allocator bench harness in `benches/alloc_count.rs`
(F38) reports 14 allocs per write across the proven matrix, well under
the R12 < 5/write target.
- **`mxaccess-rpc`** — DCE/RPC PDU codec, NTLMv2 client + server-direction
packet-integrity verify (F2 with `subtle::ConstantTimeEq`), TCP
transport, OBJREF parser + Win32 `CoMarshalInterface` emitter (F6),
`IObjectExporter::ResolveOxid` + `ResolveOxid2` (F10),
`IRemUnknown::RemQueryInterface` + `RemAddRef`/`RemRelease` (F11).
- **`mxaccess-callback`** — RPC server hosting `INmxSvcCallback` +
`IRemUnknown` for inbound `DataReceived` / `StatusReceived` frames.
`dcom_sink` (F55 Path A, gated by `windows-com`) hosts the callback
as a DCOM-managed object so `RegisterEngine2` accepts it on AVEVA
installs that do SCM-side OXID resolution against RPCSS; the
hand-rolled `CallbackExporter` is retained for unit tests.
- **`mxaccess-nmx`** — `INmxService2` client (`RegisterEngine2`,
`TransferData`, `AddSubscriberEngine`, `SetHeartbeatSendInterval`,
etc.) plus auto-resolving `NmxClient::create` factory (F12, gated by
`windows-com`).
- **`mxaccess-galaxy`** — `tiberius`-backed `Resolver` + `UserResolver`
(F14, gated by `galaxy-resolver`).
- **`mxaccess-asb-nettcp`** — `[MS-NMF]` framing + `[MC-NBFX]` binary-XML
+ `[MC-NBFS]` static dictionary + DH/HMAC/AES auth crypto with
constant-time `mod_exp` via `crypto-bigint::DynResidue` (F27).
- **`mxaccess-asb`** — `IASBIDataV2` client (`Connect`, `RegisterItems`,
`Read`, `Write`, `PublishWriteComplete`, `CreateSubscription`,
`AddMonitoredItems`, `Publish`, `DeleteMonitoredItems`,
`DeleteSubscription`, `Disconnect`) with canonical-XML HMAC signing
for all 13 `ConnectedRequest` shapes (F28) and DataContract
field-suffix names on the binary `MonitoredItem` body (F34).
- **`mxaccess`** — async Tokio façade exposing `Session`, `AsbSession`,
`Subscription` (`Stream<Item = Result<DataChange, Error>>`),
`subscribe_buffered` per R2 single-sample-with-cadence-knob
semantics (F36), `recover_connection` reconnect loop (F16), recovery
events (`RecoveryEvent::Started/Recovered/Failed`), and a typed
`Error` taxonomy. Recovery replay re-issues `RegisterReference` (not
`AdviseSupervisory`) for buffered subscriptions so the
`.property(buffer)` shape survives transport rebuild (F45);
`unsubscribe` skips the `UnAdvise` wire frame for buffered
subscriptions to match the .NET reference's `IsBuffered` guard
(F47). `Session::ensure_publisher_connected` issues the
`INmxService2::Connect` + `AddSubscriberEngine` round-trip before
the first advise against each publishing engine, so `0x33`
DataUpdate frames flow on this AVEVA install (F56). New
`WriteHandle { correlation_id }` returned by `*_with_handle` write
variants for per-operation correlation; `OperationStatus.context`
carries the originating `OperationContext` (F54). Optional
`metrics` feature emits per-op counters, latency histograms, and
connection-state gauges (F40).
- **`mxaccess-compat`** — `LMXProxyServer`-shaped Rust facade exposing
the 18-method `ILMXProxyServer5` surface as async fns over
`mxaccess::Session` / `AsbSession` with a `Mutex<HashMap<i32,
ItemRef>>` handle table and `Stream`-based event channels (F35).
`LmxClient` spawns an `operation_status_drain` fan-out task that
routes `Write` / `WriteSecured` events to `on_write_complete` and
every other op kind to `on_operation_complete`, dropping events
with unknown correlation ids silently (F54).
- **Examples** — `connect-write-read.rs`, `subscribe.rs`,
`subscribe-buffered.rs`, `asb-subscribe.rs`, `multi-tag.rs`,
`recovery.rs`, `secured-write.rs`, plus diagnostic
`asb-relay.rs`. Live-probe DoD verified end-to-end against the
AVEVA install.
- **Tooling** — `cargo public-api` baselines under
`design/public-api/{crate}.txt` with CI drift check (F41).
`design/M6-bench-baseline.md` records the alloc-count baseline.
- **Performance (post-baseline) — F52.** Three codec optimisations
measured against the F38 alloc-count harness:
- `write_message::encode_to_bytes_mut` (F52.1) — `BytesMut` output
so consumers can `split_to` / `freeze` and forward to a wire-level
sink without copying. Same alloc count as `encode`.
- Thread-local name-signature cache (F52.2) — repeated
`MxReferenceHandle::from_names` calls with the same names skip the
`to_lowercase` + CRC walk. `from_names` drops 2 → 0 allocs/op once
warm; bounded at 1024 entries per thread.
- `write_message::encode_into_bytes_mut` (F52.3) — caller-supplied
`BytesMut` scratch buffer; reusing across writes drops fixed-width
scalars from 2 → 1 alloc/op and Boolean from 1 → 0.
Bench deltas pinned in `design/M6-bench-baseline.md` § F52.{1,2,3}.
- **Live evidence — F49 / F50 / F51.** F49 step 5 (`LmxClient`
`OnWriteComplete` round-trip) verified live against AVEVA via
`cargo test -p mxaccess-compat --features live-windows-com --test
lmx_write_complete_live`. F50 captured `Suspend` (NMX opcode `0x2D`,
server-side) + `Activate` (client-side, no wire traffic) under
`captures/123-frida-suspend-advised-instrumented/` +
`captures/124-frida-activate-advised-instrumented/`; R5 settled.
F51 provisioned 7 UDAs on `$TestMachine` via `wwtools/graccesscli`
(TestFloat / TestDouble / TestDateTime / TestDuration + array
variants), captured live `AsbVariant` wire bytes for each scalar
type, and pinned them via
`crates/mxaccess-codec/tests/f51_type_matrix_parity.rs`.
- **`MxStatus` synthesizer kernel** — Path A from `Lmx.dll`
`FUN_10100ce0` ported into `MxStatus::from_packed_u32`. Settles R3
+ R4 (`OperationComplete` trigger conditions and completion-only
byte mappings: the .NET reference's `WriteCompleted` is itself a
half-implementation; the Rust port preserves the wire bytes
verbatim and routes them through the synthesizer kernel).
### Changed (vs the .NET reference)
- `NmxSubscriptionMessage::parse_data_update` accepts `record_count >= 1`;
the .NET reference hard-throws on `record_count != 1`. F44 evidence
walk against `captures/094-frida-buffered-separate-writer/`
documents the multi-record observation that drove the divergence.
- `subscribe_buffered` returns a `Stream<Item = DataChange>`
(single-sample-per-event); per R2 verification the cadence is a
server-side delivery rate knob, not a multi-sample payload.
### Known limitations
- **F3** — cross-domain NTLM Type1/2/3 fixture is permanently
out-of-scope on the dev host (single-domain only). Single-domain
wire parity is verified; cross-domain rounds-trip through the same
shape-agnostic AV-pair codec but no live fixture pins it. Self-
contained provisioning recipe (lab topology, capture procedure,
fixture layout, round-trip test skeleton) at
`docs/F3-cross-domain-ntlm-recipe.md` for whoever has access to a
two-forest Windows lab.
- **F53 (protocol crates only)** — `#![warn(missing_docs)]` is
enabled and warning-clean on the consumer-facing `mxaccess` +
`mxaccess-compat` lib roots. Protocol crates measure 1883
missing-docs warnings (mostly struct-field-level wire-shape
records); enabling the lint there would add per-field one-liners
without consumer value. Lint stays off on protocol crates
indefinitely. Per-module `#![allow(missing_docs)]` opt-out is the
re-introduction path if a contributor wants per-crate enforcement.
## Publish order
> **Note (2026-05-06, F48):** the workspace will not be published to
> crates.io. Internal usage only; consumers depend via path or git.
> The dependency DAG below is retained as a workspace-hygiene check
> (`design/F48-publish-dry-run.md` validates each crate's `cargo
> package --list` produces a clean tarball with no accidental
> captures or large files) and as the publish recipe if the policy
> ever changes (e.g. an internal contributor wants registry-style
> versioning via a private cargo registry).
Workspace crates form a dependency DAG; `cargo publish` requires
already-published deps to exist on crates.io, so the order matters:
1. `mxaccess-codec` (no internal deps)
2. `mxaccess-rpc` (no internal deps)
3. `mxaccess-asb-nettcp` (no internal deps)
4. `mxaccess-galaxy` (depends on codec)
5. `mxaccess-callback` (depends on rpc + codec)
6. `mxaccess-asb` (depends on codec + asb-nettcp)
7. `mxaccess-nmx` (depends on codec + galaxy + rpc + callback)
8. `mxaccess` (depends on all the above)
9. `mxaccess-compat` (depends on mxaccess)
`cargo publish --dry-run` validates each crate's metadata + tarball
in isolation; the dependent crates' dry-runs require the leaf crates
to actually exist on crates.io (the registry lookup happens regardless
of `--no-verify`). For pre-publish verification: leaf crates dry-run
in CI; dependent crates are validated by the public-api baseline +
build-test-clippy matrix.