Lands M2 wave 1 — three pure-Rust modules under crates/mxaccess-rpc with 60 unit tests. Each is a 1:1 port of one .NET reference file: - ntlm.rs (1137 LoC, 19 tests) — `ManagedNtlmClientContext.cs`. NTLMv2 challenge/response, Type1/Type3 builders, sign() with RC4-sealed checksum and per-call sequence advance. Manual `Debug` impl that hides credentials; not Clone (rc4 0.2 cipher state is non-Clone). Pure-Rust crypto via hmac/md-5/md4/rc4 v0.2/rand v0.8 (rc4 0.2 chosen per design/review.md:78). - pdu.rs (1573 LoC, 33 tests) — `DceRpcPdu.cs` + auth-trailer types from `DceRpcAuthentication.cs`. Bind/AlterContext/Auth3/Request/Response/Fault PDUs, NDR20 transfer syntax, auth_value with 4-byte alignment padding, preserved-byte fields per CLAUDE.md unknown-bytes rule. - objref.rs (~470 LoC, 11 tests including a 366-byte captured OBJREF round-trip) — `ComObjRef.cs`. MEOW signature, OXID/OID/IPID, dual-string array with printable-ASCII escaping and security-binding boundary. ComObjRefProvider.cs deferred (windows-rs Win32 wrapper — see F6). Every wire-byte claim cites src/MxNativeClient/<file>.cs:LINE per CLAUDE.md "no fabricated protocol behaviour" rule. Test count delta: 217 → 277 (+60) Open followups touched: F1–F8 (new — see design/followups.md) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.4 KiB
Followups
Open work items deferred during /loop iterations. Triaged at the top of
every iteration. New items are appended under ## Open; resolved items
move to ## Resolved with a date + commit hash.
Open
F1 — NTLM consumer-layer helpers (workstation default + from_env constructor)
Severity: P3
Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs
Why deferred: The .NET reference's Environment.MachineName default for workstation and its FromEnvironment() constructor (ManagedNtlmClientContext.cs:38, :41-49) read host state and env vars — both side effects that don't belong in a pure codec module. The constructor takes workstation: Option<&str> so callers can wire either later.
Resolves when: M2 wave 2 transport (or the M2 example connect-nmx.rs) wires NtlmClientContext::new(.., Some(hostname()?)) and provides a small from_env helper at the consumer layer.
F2 — NTLM verify_signature path + constant-time MAC compare (server-to-client direction)
Severity: P2
Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs
Why deferred: The .NET ManagedNtlmClientContext only implements client-to-server signing (cs:30,124); there is no implementation of server-to-client sign/seal keys or verify_signature. Both are needed when the callback exporter receives a signed inbound frame from NmxSvc.exe, but no such fixture exists yet.
Resolves when: M2 wave 3 (callback exporter) captures an INmxSvcCallback::StatusReceived frame with an auth_value trailer per design/60-roadmap.md:56 (DoD #3) and a fixture lands under tests/fixtures/m2-status-frame/. Add subtle = "2" and gate the byte compare behind ConstantTimeEq at the same time.
F3 — Cross-domain NTLM Type1/2/3 fixture
Severity: P2
Source: M2 wave 1, crates/mxaccess-rpc/src/ntlm.rs
Why deferred: All current NTLM fixtures are single-domain (the local AVEVA install). Tracked separately in design/70-risks-and-open-questions.md R8 (P1 risk) and the open-evidence-gaps table.
Resolves when: A multi-domain AVEVA test harness lands and a successful cross-domain authenticate round-trip captures Type1/2/3 bytes. Notes: this clears R8.
F4 — BindAck / AlterContextResponse body parser
Severity: P2
Source: M2 wave 1, crates/mxaccess-rpc/src/pdu.rs
Why deferred: The .NET reference (DceRpcPdu.cs:217-262) parses Bind and AlterContext into the same struct but does not decode the corresponding response body (result list + secondary address). The Rust port's BindPdu::decode accepts BindAck packet type but does not interpret the body. The negotiated transfer syntax — needed before opnum dispatch — is currently inferred from request-side context.
Resolves when: A captured BindAck frame from captures/013-loopback-subscribe-scalars/nmx-stream-*.bin is decoded and the body shape is documented in docs/Loopback-Protocol-Findings.md.
F5 — Captured DCE/RPC bind-frame fixture round-trip
Severity: P2
Source: M2 wave 1, crates/mxaccess-rpc/src/pdu.rs
Why deferred: Existing PDU tests build hand-constructed [C706]-conformant frames. A capture-driven round-trip (extract bind/alter PDUs from captures/013-loopback-subscribe-scalars/nmx-stream-*.bin, decode → encode → assert byte-identical) would be stronger evidence of parity with the live wire.
Resolves when: Bytes from that capture are extracted into tests/fixtures/m2-pdu/ and the round-trip test lands.
F6 — Port ComObjRefProvider.cs (OBJREF emitter via Win32 CoMarshalInterface)
Severity: P2
Source: M2 wave 1, crates/mxaccess-rpc/src/objref.rs
Why deferred: The provider is a wrapper around ole32::CoMarshalInterface / IStream / GlobalLock / GlobalSize. It needs windows-rs, which is currently behind the windows-com feature in mxaccess-rpc/Cargo.toml. The pure-Rust parser stands alone for the inbound activation-response path that M2 wave 1 needs.
Resolves when: windows-rs is wired into mxaccess-rpc (M2 wave 3 callback exporter needs to publish its own OBJREF for IRemUnknown / INmxSvcCallback registration) and an emitter port lands behind the windows-com feature.
F7 — Consolidate Guid type across mxaccess-rpc
Severity: P3
Source: M2 wave 1, crates/mxaccess-rpc/src/{objref.rs,pdu.rs}
Why deferred: objref::Guid is a self-contained [u8; 16] newtype with Display matching .NET Guid.ToString("D"). pdu::SyntaxId uses raw [u8; 16] for IIDs. Both work but a single shared type would be cleaner.
Resolves when: A small consolidation lands — either pub use objref::Guid as Guid; from pdu, or both move to a shared crate::guid module. Trivial; pick during M2 wave 2 when the next agent touches the crate.
F8 — RpcError is duplicated across objref and pdu modules
Severity: P3
Source: M2 wave 1, crates/mxaccess-rpc/src/{objref.rs,pdu.rs}
Why deferred: Each module defined its own RpcError enum with a partial set of variants. Both are sound in isolation but the crate-public RpcError should be a single union. Not blocking — they don't collide because each module re-exports its own.
Resolves when: M2 wave 2 (OXID + IRemUnknown::RemQueryInterface) needs a third error surface. At that point, hoist RpcError to crates/mxaccess-rpc/src/error.rs mirroring mxaccess-codec/src/error.rs, and have each module use the shared enum.
Resolved
(none yet)