gRPC M0: probe (R0.4, live-verified) + system-param (R0.3) + shared handshake

Roadmap docs/plans/hcal-roadmap.md, milestone M0 (gRPC parity for the DONE
surface). Now unblocked for live verification by a reachable 2023 R2 server.

- R0.4 Probe over gRPC: new HistorianGrpcProbe calls History/Retrieval/Status
  GetInterfaceVersion (unauthenticated). ProbeAsync routes over gRPC when
  Transport==RemoteGrpc. LIVE-VERIFIED against a real 2023 R2 server — needs no
  credentials (runs before the auth loop), so it works despite the auth blocker.

- R0.3 System parameter over gRPC: new HistorianGrpcStatusClient calls
  StatusService.GetSystemParameter over the authenticated session; routed in the
  dialect. Built + unit-tested (request/response field mapping pinned).
  Live-verification pending an auth fix (see below).

- Extracted the proven auth handshake from HistorianGrpcReadOrchestrator into
  shared Grpc/HistorianGrpcHandshake (reused by read + status + future
  browse/metadata). Repointed the IL structural guardrail test to it.

- Diagnostics: round-failure now decodes the native server error + hex/ASCII
  preview (HistorianNativeHandshake.DescribeError). This surfaced the live auth
  blocker as SEC_E_LOGON_DENIED (0x8009030C) at NTLM round 1 — framing is correct,
  the credential did not validate. Probable cause: stale file password or NAM-domain
  NTLM restriction (Kerberos/RDP works, NTLM denied; no SPN path over the tunnel).

216 unit tests pass; live gRPC probe passes. Sanitization scan clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
This commit is contained in:
Joseph Doherty
2026-06-21 13:32:04 -04:00
parent 22e9c5e5f8
commit c4b8d0dde4
11 changed files with 293 additions and 56 deletions
@@ -11,7 +11,7 @@ namespace AVEVA.Historian.Client.Tests;
/// credentials (live-confirmed against a real 2023 R2 server, 2026-06-21). An earlier revision
/// routed the loop to ExchangeKey; this test fails if that regression returns.
///
/// It works by disassembling the IL of <c>HistorianGrpcReadOrchestrator</c> (and its
/// It works by disassembling the IL of <c>HistorianGrpcHandshake</c> (and its
/// compiler-generated nested closure types — the token-loop call lives inside a lambda) and
/// collecting every method invoked.
/// </summary>
@@ -20,8 +20,10 @@ public sealed class HistorianGrpcHandshakeRoutingTests
[Fact]
public void Handshake_UsesValidateClientCredential_NotExchangeKey()
{
// The auth token loop lives in the shared handshake helper (reused by the read, status,
// and future browse/metadata gRPC paths).
HashSet<string> calledMethods = CollectCalledMethodNames(
"AVEVA.Historian.Client.Grpc.HistorianGrpcReadOrchestrator");
"AVEVA.Historian.Client.Grpc.HistorianGrpcHandshake");
Assert.Contains("ValidateClientCredential", calledMethods);
Assert.DoesNotContain("ExchangeKey", calledMethods);