# 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. ### F10 — `IObjectExporter::ResolveOxid2` (opnum 4) body codec **Severity:** P2 **Source:** M2 wave 2, `crates/mxaccess-rpc/src/object_exporter.rs` **Why deferred:** `ObjectExporterMessages.cs` only models opnum 0 (`ResolveOxid`). Opnum 4 (`ResolveOxid2`) has a different response shape — it adds a `COMVERSION` plus an `AuthnHnt[]` array. The .NET reference does not exercise this path, so there's no executable spec to mirror. **Resolves when:** Either a `[MS-DCOM]` §3.1.2.5.1.4-derived layout is verified against a captured `ResolveOxid2` exchange, or the .NET reference grows a `ParseResolveOxid2*` helper. ### F11 — `IRemUnknown::RemAddRef` and `RemRelease` body codecs **Severity:** P2 **Source:** M2 wave 2, `crates/mxaccess-rpc/src/rem_unknown.rs` **Why deferred:** `RemUnknownMessages.cs` declares the opnums (`:9-10`) but does not implement encoders/decoders. The Rust port matches that exactly per "port what is already proven." **Resolves when:** The .NET reference adds bodies for opnums 4 / 5 (or a captured frame establishes the on-wire shape). At that point port them into `rem_unknown.rs` alongside the existing `RemQueryInterface` codec. ### F12 — `NmxClient::create` (auto-resolving COM-activation factory) **Severity:** P1 **Source:** M3 stream B, `crates/mxaccess-nmx/src/client.rs` **Why deferred:** `ManagedNmxService2Client.Create()` (`ManagedNmxService2Client.cs:30-64`) auto-discovers `(host, port, service_ipid)` by activating the `NmxSvc.NmxService` COM ProgID, marshalling the resulting `IUnknown` to an OBJREF, calling `IObjectExporter::ResolveOxid` against the OXID inside, then `IRemUnknown::RemQueryInterface` to get the `INmxService2` IPID. This requires `windows-rs` for `CoCreateInstance` / `CLSIDFromProgID` (the same gating dep as F6), plus the `ComObjRefProvider.MarshalIUnknownObjRef` port (also F6). **Resolves when:** F6 lands (windows-rs wired in + `ComObjRefProvider` port). At that point `NmxClient::create()` becomes ~30 lines that chain the existing primitives: COM activation → `MarshalIUnknownObjRef` → `ComObjRef::parse` → `object_exporter_client::resolve_oxid_with_managed_ntlm_packet_integrity` → `rem_unknown::encode_rem_query_interface_request` over a temporary transport → `NmxClient::connect`. ### F14 — `tiberius`-backed SQL implementation of `Resolver` + `UserResolver` **Severity:** P2 **Source:** M3 stream A, `crates/mxaccess-galaxy/src/sql.rs` (constants present, no client wiring yet) **Why deferred:** `tiberius` is the recommended Rust SQL Server client; pulling it as a non-default dep means the `mxaccess-galaxy` crate keeps a slim default footprint (consumers can plug their own `Resolver` / `UserResolver` impl without dragging in TDS / native-tls / winauth). The actual `GalaxyRepositoryTagResolver` and `GalaxyRepositoryUserResolver` impls are short — they just bind the canonical SQL constants in `crate::sql` (`RESOLVE_SQL`, `BROWSE_SQL`, `USER_BY_GUID_SQL`, `USER_BY_NAME_SQL`) and translate `tiberius::Row` → typed `GalaxyTagMetadata` / `GalaxyUserProfile`. **Resolves when:** A `tiberius`-backed module lands behind the existing `galaxy-resolver` Cargo feature flag in `mxaccess-galaxy/Cargo.toml`. Live-probe gating: needs a Galaxy DB to verify against (`MX_GALAXY_DB` env var, populated by `tools/Setup-LiveProbeEnv.ps1`). The pure-Rust foundation (data types, parser, trait, SQL strings) is already in place — this is "fill in the backend" rather than "design the surface." ### F13 — `NmxClient` high-level write/advise/subscribe wrappers **Severity:** P1 **Source:** M3 stream B, `crates/mxaccess-nmx/src/client.rs` **Why deferred:** The .NET `Write`, `Write2`, `WriteSecured2`, `AdviseSupervisory`, `SendObservedPreAdviseMetadata`, `RegisterReference`, and `UnAdvise` methods (`ManagedNmxService2Client.cs:303-466`) are short — each builds an inner NMX message via `mxaccess-codec` (`NmxWriteMessage`, `NmxItemControlMessage`, `NmxSecuredWrite2Message`, `NmxMetadataQueryMessage`, `NmxReferenceRegistrationMessage`), wraps it in an `NmxTransferEnvelope`, then calls `TransferData(...)`. They depend on `GalaxyTagMetadata` from M3 stream A (the Galaxy SQL resolver) for `PlatformId` / `EngineId` / `ToReferenceHandle(galaxyId)`. **Resolves when:** M3 stream A (`mxaccess-galaxy`) lands `GalaxyTagMetadata` (or an equivalent type) and `MxReferenceHandle` from `mxaccess-codec` is wired to it. At that point ~120 lines of wrappers go into `NmxClient` that delegate to the existing `transfer_data` opnum. ## Resolved ### F7 — Consolidate `Guid` type across `mxaccess-rpc` **Resolved:** 2026-05-05 in this iteration's commit. `Guid` was hoisted from `objref::Guid` into the new shared `crate::guid::Guid` module. `objref` and `pdu` now re-export from there; M2 wave 2's `orpc`, `object_exporter`, and `rem_unknown` import it directly. The OXID-resolve dual-string decoder additionally needs an owned protocol label (`format!("protseq_0x{:04x}", tower_id)` per `ObjectExporterMessages.cs:120`) — `ComDualStringEntry::protocol` was upgraded from `&'static str` to `Cow<'static, str>` to support both decoders without the agent's interim `Box::leak` workaround. ### F8 — `RpcError` is duplicated across `objref` and `pdu` modules **Resolved:** 2026-05-05 in this iteration's commit. `RpcError` was hoisted into the new shared `crate::error::RpcError` module as a single union of all wave 1 variants plus a generic `Decode { offset, reason: &'static str, buffer_len }` variant for the wave 2 ORPC parsers' one-off failures. `objref` and `pdu` re-export from there; M2 wave 2's `orpc`, `object_exporter`, and `rem_unknown` use it directly. ### F9 — `ObjectExporterClient.cs` ResolveOxid wrapper methods **Resolved:** 2026-05-05. Both portable methods land in `crates/mxaccess-rpc/src/object_exporter_client.rs`: `resolve_oxid_unauthenticated` (mirrors `cs:14-30`) and `resolve_oxid_with_managed_ntlm_packet_integrity` (mirrors `cs:66-81`). Each opens a TCP connection, binds to `IObjectExporter`, calls opnum 0 with the encoded request, and decodes the response — preferring `parse_resolve_oxid_result` then falling back to `parse_resolve_oxid_failure` for short stubs. The two SSPI flavours (`ResolveOxidWithNtlmConnect`, `ResolveOxidWithNtlmPacketIntegrity`) wrap .NET's `System.Net.Security.SspiClientContext` and are explicitly out of scope for the Rust port — that's a permanent skip, not a deferral.