Files
histsdk/CLAUDE.md
T
Joseph Doherty 7a3cd9b76e Resolve write-path silent fails + expand EnsureTagAsync, RetrievalMode coverage
DelT and EnsT2 had two distinct silent-fail blockers; both now resolved live
end-to-end. Read path's RetrievalMode mapping was missing 11 of 15 enum values
(plus a latent Cyclic→4 bug). Investigation tooling kept as env-gated helpers.

DelT silent fail: Open2 was using NativeIntegratedReadOnlyConnectionMode (0x402);
server returned err 132 OperationNotEnabled silently. Added
NativeIntegratedWriteEnabledConnectionMode (0x401) per
HistorianAccessUtil.SetConnectionMode bit map (Process=1 | IntegratedSecurity=0x400).
Write orchestrator now opens with write-enabled mode.

EnsT2 silent fail: byte-by-byte comparison via inspector revealed two bugs in
SerializeAnalogCTagMetadata. The original "146-byte byte-for-byte match" was
misaligned — it omitted the leading 0x4E marker byte and treated WCF's `01 01 01`
EndElement closing markers as if they were part of the InBuff payload. Real native
InBuff is 144 bytes with 0x4E lead and 2-byte `FE 00` trailer. Golden test bytes
corrected.

EnsureTagAsync expansion: probed every analog data type via
instrument-wcf-writemessage; byte 11 of CTagMetadata is the data-type
discriminator (Float=0x01, Double=0x21, UInt2=0x09, UInt4=0x11, Int2=0x29,
Int4=0x31). String/Int1/Int8/UInt8 fail at native AddTag — out of scope for
this op. Range encoding decoded: defaults emit compact `1A 03`; non-default
emit `1F 00` + 4 doubles in order MinEU/MaxEU/MinRaw/MaxRaw. MinRaw/MaxRaw
sent on the wire but server mirrors them to MinEU/MaxEU when ApplyScaling=false
(verified against native — server quirk, not SDK bug).

RetrievalMode mapping: probed all 15 enum values; QueryType is just the native
enum ordinal. Replaced the broken switch with `(uint)mode`. Existing SDK
mapped Cyclic→4 (BestFit's value); Cyclic is actually 0.

CLAUDE.md updated: stale "Active Protocol Blocker" rewritten as resolved-status
block; SDK surface now reflects the read-blocker resolution and the new write
ops; "Remaining gaps" punch list refreshed.

Tools added (both env-gated, no runtime overhead unless flipped on):
- HistorianWcfMessageCaptureBehavior — captures all WCF body bytes when
  AVEVA_HISTORIAN_SDK_WIRE_CAPTURE is set; used for byte-level diff vs native.
- HistorianWcfHistAddressingBehavior — explicitly sets wsa:To header on the
  Hist channel for parity with native bytes (kept though not load-bearing).
- WriteDiag in TagWriteOrchestrator — env-gated EnsT2/DelT response logging
  (AVEVA_HISTORIAN_DELT_DIAG).

NativeTraceHarness CLI: added --write-min-eu/--write-max-eu/--write-min-raw/
--write-max-raw for capturing non-default-range EnsT2 payloads.

Tests: 130 → 161 passing (+31). Includes 16-mode RetrievalMode mapping table,
4 per-data-type EnsT2 golden tests, NonDefaultRanges golden test, 6 live
round-trip integration tests covering Float/Double/Int2/Int4/UInt4/FloatRanges,
3 live tests for previously-unmapped RetrievalMode values.

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

127 lines
11 KiB
Markdown
Raw 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.
# 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
```powershell
dotnet build .\Histsdk.slnx --no-restore
dotnet test .\Histsdk.slnx --no-build --logger "console;verbosity=minimal"
```
Run a single test:
```powershell
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:
```powershell
$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:
```powershell
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 (`HistorianWcfTagClient` line ~357) throws — only integrated security wired for that op.
- `Historian2020ProtocolDialect.GetTagInfoByName/GetTagInfos` throws — currently dead code; `GetTagMetadataAsync` works through the WCF tag client instead.
- 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 not a Git working tree in the current checkout — track changes via file timestamps or external backup.