Files
histsdk/CLAUDE.md
T
Joseph Doherty f32fd57874 Remove dead dialect methods; unblock explicit-creds tag-metadata path
Two cleanups from the post-EnsureTagAsync punch list — both isolated, no
protocol discovery required.

#89 dead code in Historian2020ProtocolDialect:
  - BrowseTagNamesAsync and GetTagMetadataAsync on the dialect both threw
    ProtocolEvidenceMissingException, but HistorianClient routes those calls
    directly to HistorianWcfTagClient — the dialect overrides were never
    reached. Removed both methods. ReadBlocksAsync stays (it's a deliberate
    guardrailed entry on the public surface).

#90 explicit-creds tag-metadata path:
  - HistorianWcfTagClient.WcfRetrievalSession.ValidateSupportedAuth threw
    ProtocolEvidenceMissingException whenever IntegratedSecurity=false AND
    UserName/Password were supplied. But the surrounding code already wires
    those creds through ApplyWindowsCredential ->
    factory.Credentials.Windows.ClientCredential — the validator was just
    being conservative about an untested combination.
  - Inverted the check: now only rejects the no-auth-at-all combination
    (IntegratedSecurity=false + no UserName + no Password). The other three
    valid auth shapes pass through to WCF.

Tests: 161 -> 163 (+2). New unit test verifies the no-auth case still
throws; new gated live integration test
GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian exercises the
explicit-creds path when HISTORIAN_USER+HISTORIAN_PASSWORD are set, skips
cleanly otherwise.

CLAUDE.md updated: removed the two now-resolved entries from "Remaining
gaps"; explicit-creds line refined to note the live-verification env-var
requirement.

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

11 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Mission

Build a fully managed .NET 10 replacement for AVEVA Historian's aahClientManaged / aahClient.dll stack by reverse-engineering the proprietary binary protocol. The production SDK under src/AVEVA.Historian.Client/ must remain pure managed .NET 10 — no P/Invoke, no native AVEVA runtime dependency, no REST. Tools under tools/ and scripts under scripts/ are reverse-engineering aids only.

Read AGENTS.md (standing constraints), instructions.md (decision record), and docs/reverse-engineering/handoff.md (current evidence + active blocker) before starting non-trivial work. The handoff doc is the entry point — it tracks the live blocker, next pickup steps, and the canonical list of primary reference docs.

Required SDK Surface

Reads (the original required surface, all working live as of 2026-05-04):

  • ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
  • BrowseTagNamesAsync, GetTagMetadataAsync
  • Status helpers: GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync

Writes (added 2026-05-04 by explicit user request — do not extend further without one):

  • EnsureTagAsync for analog types: Float, Double, Int2, Int4, UInt4 (live-verified end-to-end). Other types (SingleByteString/DoubleByteString/Int1/Int8/UInt8) fail at native AddTag — likely require a different path and are intentionally not supported. MinEU/MaxEU round-trip correctly into the DB; MinRaw/MaxRaw are sent on the wire but the server mirrors them to MinEU/MaxEU when ApplyScaling=false (verified against native — server quirk, not SDK bug).
  • DeleteTagAsync

AddS2 (write samples) is architecturally blocked — server cache only ingests from configured IOServers/ApplicationServer pipelines. Do not add write-samples support.

Methods without protocol evidence currently throw ProtocolEvidenceMissingException from Historian2020ProtocolDialect. Do not stub fake behavior — leave them throwing until evidence supports an implementation.

Build & Test

dotnet build .\Histsdk.slnx --no-restore
dotnet test .\Histsdk.slnx --no-build --logger "console;verbosity=minimal"

Run a single test:

dotnet test .\Histsdk.slnx --no-build --filter "FullyQualifiedName~WcfDataQueryProtocolTests"

Live integration tests in tests/AVEVA.Historian.Client.Tests/HistorianClientIntegrationTests.cs are gated and skip cleanly without these env vars:

$env:HISTORIAN_HOST, $env:HISTORIAN_PORT (32568), $env:HISTORIAN_USER, $env:HISTORIAN_PASSWORD,
$env:HISTORIAN_TEST_TAG, $env:HISTORIAN_TAG_FILTER

Never write real credentials, hostnames, user names, or customer tag names into docs, scripts, captures, or commit messages.

Reverse-Engineering CLI

tools/AVEVA.Historian.ReverseEngineering is the .NET 10 CLI for static inspection, WCF probes, and IL-rewrite instrumentation. Common entry points:

dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-probe $env:HISTORIAN_HOST 32568
dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-cert-probe $env:HISTORIAN_HOST 32568 localhost
dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-like-tag-browse $env:HISTORIAN_HOST 32568 $env:HISTORIAN_TAG_FILTER
dotnet run --no-build --project tools\AVEVA.Historian.ReverseEngineering -- wcf-start-query $env:HISTORIAN_HOST 32568 $env:HISTORIAN_TEST_TAG --max-attempts 1 --timeout-seconds 3
dotnet run --project tools\AVEVA.Historian.NativeTraceHarness -- --scenario history --tag $env:HISTORIAN_TEST_TAG --lookback-minutes 1440

The wcf-start-query matrix is expensive — always pass --max-attempts / --timeout-seconds for negative probes. See docs/reverse-engineering/capture-workflow.md for the full repeatable capture sequence (manifest, mark, exports, Frida winsock attach, etc.).

Code Architecture

Production SDK (src/AVEVA.Historian.Client/)

Three layered subsystems, intentionally decoupled so protocol parsing can be unit-tested without a live server:

  • HistorianClient + HistorianClientOptions — public façade. Validates inputs, delegates reads to Historian2020ProtocolDialect, delegates probe/tag-metadata/browse to the WCF layer.
  • Wcf/ — managed WCF/MDAS layer. The Historian uses Net.TCP on port 32568 with a custom application/x-mdas content type wrapping a binary SOAP 1.2 / WS-Addressing 1.0 envelope. MdasMessageEncoder + MdasMessageEncodingBindingElement implement that wrapper. HistorianWcfBindingFactory produces three flavors: plain MDAS, MDAS+Windows transport (used for /Hist-Integrated), and MDAS+certificate (used for /HistCert). Service paths live in HistorianWcfServiceNames. WCF data contracts (Wcf/Contracts/) are reproduced from server-side static analysis and are versioned per native interface (e.g., IRetrievalServiceContract2..4).
  • Protocol/ — binary frame layer (HistorianFrameReader/Writer, HistorianBinaryPrimitives, HistorianMessageType). Historian2020ProtocolDialect is the version-anchored bridge between HistorianClient and the frame layer; methods without sufficient evidence throw ProtocolEvidenceMissingException rather than guessing wire bytes.
  • Transport/ — pluggable IHistorianTransport (default: TCP). Tests inject a fake transport.
  • Models/ — public DTOs and enums (HistorianSample, RetrievalMode, etc.). HistorianDataValue represents the discriminated value type.

InternalsVisibleTo exposes internals to the test assembly and the reverse-engineering tool.

Read-path status (resolved 2026-05-04)

The original blocker — Open2 reaching server logic but Retr.StartQuery2 returning false with empty buffers — is resolved. Root causes were:

  1. WCF parameter-name mismatches — server contracts use inBuff/outBuff/pRequestBuff/etc.; the SDK's default C#-derived names made the deserializer ignore the body. Fixed via [MessageParameter(Name = "...")] attributes across IHistoryServiceContract2, IRetrievalServiceContract2..4, IStatusServiceContract2, ITransactionServiceContract.
  2. Native SSPI request flags — round 0 = 0x2081C (adds IDENTIFY + REPLAY_DETECT + SEQUENCE_DETECT); rounds 1+ = 0x81C. Without REPLAY_DETECT|SEQUENCE_DETECT, NTLM MIC generation is skipped and AcceptSecurityContext rejects round 1. Implemented in HistorianSspiClient via P/Invoke InitializeSecurityContextW.
  3. Cross-service version probes (Trx/GetV, Stat/GetV, Retr/GetV) between RTag2 and EnsT2 in the event flow — required to register the client with each service's session table.

End-to-end chain working from a pure managed .NET 10 client: Hist.GetV → Hist.ValCl × N → Hist.Open2 → /Retr.GetV → Retr.IsOriginalAllowed → Retr.StartQuery2 → loop Retr.GetNextQueryResultBuffer2. 23 live integration tests against localhost cover all required reads + the two write ops.

Write-path notes (added 2026-05-04)

EnsureTagAsync and DeleteTagAsync chain follow the same pattern as reads but require Open2 with NativeIntegratedWriteEnabledConnectionMode = 0x401 (Process | Write | IntegratedSecurity) — the read-path's 0x402 (read-only) makes the server return err 132 OperationNotEnabled silently. The analog Float CTagMetadata payload is 144 bytes with a leading 0x4E marker byte and a 2-byte trailer FE 00. See docs/reverse-engineering/handoff.md and the WriteDiag env-gated diagnostic helper in HistorianWcfTagWriteOrchestrator for capture details.

Remaining gaps

Smaller, isolated items — none block the production read surface:

  • Remote TCP transports (RemoteTcpIntegrated, RemoteTcpCertificate) untested against an actual remote Historian (tests skip without HISTORIAN_REMOTE_TCP_HOST).
  • Explicit username/password tag-metadata path is wired (validator only blocks no-auth-at-all), but live-verification requires HISTORIAN_USER+HISTORIAN_PASSWORD set; gated test GetTagMetadataAsync_ExplicitCredentials_AgainstLocalHistorian skips otherwise.
  • Per-row trailing ~24 bytes of GetNextQueryResultBuffer are not decoded (likely per-sample value/source/state metadata).
  • EnsureTagAsync distinct MinRaw/MaxRaw persistence requires ApplyScaling=true + a follow-up UpdateTags call — not yet wired (no API user has asked).

Tools Layer

  • tools/AVEVA.Historian.NativeTraceHarness/.NET Framework (not .NET 10) harness that loads current/aahClientManaged.dll and records sanitized reflection snapshots around OpenConnection, StartQuery, MoveNext. Exists specifically to parity-test against the native wrapper.
  • tools/AVEVA.Historian.NetFxWcfProbe/ — .NET Framework WCF probe to rule out .NET 10-only WCF behavior differences.
  • tools/AVEVA.Historian.ReverseInstrumentation/ — assembly injected into IL-rewritten copies of aahClientManaged.dll for sanitized logging. Rewrites land in docs/reverse-engineering/dnlib-write-copy/, never in current/.
  • tools/AVEVA.Historian.WcfCaptureServer/ — fake server for endpoint experiments.
  • scripts/ — PowerShell + Frida runners for native attach captures (winsock, system boundary, runtime pointers, ValCl SSPI context).

Evidence & Artifacts

  • docs/reverse-engineering/ — sanitized Markdown summaries + small JSON evidence. Always commit-safe.
  • artifacts/reverse-engineering/ — raw / identity-bearing runtime output. Never committed; never copy contents into docs/ without sanitizing.
  • fixtures/protocol/ — sanitized golden byte fixtures, named to match manifest scenarios.
  • current/ and aveva-install-{x64,x86}/ — AVEVA binaries. Never modify, delete, or redistribute. Use current/ first because it matches the deployed sidecar.

Testing Conventions

Unit tests are golden-byte and round-trip oriented — WcfDataQueryProtocolTests, WcfEventQueryProtocolTests, WcfTagQueryProtocolTests, WcfOpen2ProtocolTests, FrameTests, BinaryPrimitiveTests. ProtocolGuardrailTests enforces that unimplemented methods throw ProtocolEvidenceMissingException rather than returning empty results. When adding a new protocol code path, add a golden-byte fixture before/alongside the implementation.

Safety

  • Never commit credentials, hostnames, user names, customer tag names, or raw packet captures. Use placeholders in docs.
  • Run a sanitization scan after touching auth/capture docs (the rg pattern is in handoff.md "Next Pickup Steps").
  • Production code under src/ must remain pure managed .NET 10 with no native AVEVA reference. Reverse-engineering harnesses under tools/ may reference native binaries.
  • This workspace IS a Git working tree (origin: gitea.dohertylan.com). Use normal git workflow; the prior note about "no working tree, track via timestamps" is obsolete.