11 Commits

Author SHA1 Message Date
Joseph Doherty 549995e4a9 CLAUDE.md: cross-platform cert-binding verified end-to-end
With AllowUntrustedServerCertificate=true + ServerDnsIdentity="localhost",
all four representative read calls (ReadRawAsync, GetSystemParameterAsync,
BrowseTagNamesAsync, GetTagMetadataAsync) succeed from a Debian 13 client
against the Windows Historian over RemoteTcpCertificate with explicit
Windows credentials and NegotiateAuthentication via GSSAPI/NTLM.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 23:09:21 -04:00
Joseph Doherty 92d4110142 CLAUDE.md: cross-platform end-to-end verified
Verified from a Debian 13 client (.NET 10.0.203) against the Windows
Historian using explicit Windows credentials and NegotiateAuthentication
via GSSAPI/NTLM:

- GetTagMetadataAsync: returns correct fields for SysTimeSec
- BrowseTagNamesAsync: returns SysTimeHour, SysTimeMin, SysTimeSec
- ProbeAsync: works over both transports

Calls that touch the cert-transport binding directly (ReadRawAsync,
GetSystemParameterAsync) still fail at X509 chain validation despite
update-ca-certificates. .NET WCF on Linux uses its own X509Chain plumbing
rather than the system CA bundle. Documented as a follow-up rather than
fixed in this pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:53:10 -04:00
Joseph Doherty 8607f5d530 CLAUDE.md: document cross-platform status
Verified 2026-05-04:
- SDK builds on Debian 13 with .NET 10 SDK 10.0.203
- ProbeAsync over RemoteTcpCertificate works from Linux
- RemoteTcpIntegrated fails on Linux due to a WCF-level limitation
  (NetTcpBinding + Windows TcpClientCredentialType is BCL-Windows-only),
  not a HistorianSspiClient issue
- Authenticated reads over the cert path with explicit creds are wired
  but await live verification

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:29:45 -04:00
Joseph Doherty 7e4d713eb3 Cross-platform NegotiateAuthentication; StorageType field; docs polish
HistorianSspiClient rewritten on top of System.Net.Security.NegotiateAuthentication
in place of P/Invoke into secur32.dll's InitializeSecurityContextW. The class
keeps the same Next() / Dispose() / two-constructor surface so callers don't
change. RequiredProtectionLevel=EncryptAndSign + AllowedImpersonationLevel=
Identification produces a request-flag set equivalent to the captured native
0x2081C / 0x81C bitmasks (still preserved as constants for the existing unit
tests). Removes the only Windows P/Invoke in the production SDK; the
[SupportedOSPlatform("windows")] gating elsewhere stays in place pending a
separate sweep.

HistorianStorageType (Cyclic = 1, Delta = 2):
Captured 2026-05-04 via --write-storage-type on the harness. Delta differs
from Cyclic in three places — header byte 10 (0x02 -> 0x06), flag-block
byte 1 (0x01 -> 0x02), and 4 zero bytes inserted after StorageRate before
the FILETIME. Server persists Tag.StorageType=1/2 accordingly. Plumbed
through HistorianTagDefinition.StorageType + serializer + orchestrator + 2
new tests (golden bytes + live SQL persistence verification).

Docs polish:
CLAUDE.md no longer claims "no P/Invoke" (HistorianSspiClient is the one
allowed P/Invoke surface); updated test count to 169+; AGENTS.md Required
SDK Surface and Repository Layout brought up to date with the live state
including the write surface; handoff.md "not a git working tree" obsolete
note removed.

171/171 tests pass with the NegotiateAuthentication replacement (was 169;
+2 new tests for StorageType).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:19:37 -04:00
Joseph Doherty 5ce62a5900 Wire ApplyScaling, StorageRate; close out write-commands plan
ApplyScaling (HistorianTagDefinition.ApplyScaling):
The EnsT2 trailer's second byte controls server-side scaling — `FE 00`
mirrors MinRaw to MinEU and sets AnalogTag.Scaling=0; `FE 01` persists
distinct MinRaw/MaxRaw and sets Scaling=1. Decoded by toggling
set_ApplyScaling on the native harness and capturing the wire bytes for
both values with identical inputs. The earlier docs claimed
EnsureTagAsync needed a follow-up "UpdateTags" call; the WCF surface has
no such operation — toggling that one byte is the whole fix.

StorageRate (HistorianTagDefinition.StorageRateMs):
Serializer accepts a non-default rate, validated empirically against
the live server which only accepts quantized values
(1000/5000/10000/60000/300000 ms).

EnsureTagAsync upsert semantics:
Second call on the same tag name with different fields succeeds and
updates Description, MinEU, MaxEU, MinRaw, MaxRaw, Scaling in place
(verified by direct SQL inspection in a live test).

Plan + doc closeout:
write-commands-reverse-engineering.md rewritten as a current-state
plan with three workstreams (A doc closeout / B idempotency / C1
StorageRate) and a parallelism table; prior phase notes preserved as
appendix. handoff.md, implementation-status.md, wcf-contract-evidence.md,
README.md updated to remove "writes are out of scope" / non-existent
UpdateTags references and document the actual EnsT2 wire format
including the `FE xx` trailer.

Reverse-engineering harness gains --write-apply-scaling and a SQL
post-check that prints the persisted AnalogTag bounds so future RE
sessions can verify wire→DB causality without leaving the harness.

169/169 tests pass (was 165; +4 new tests covering ApplyScaling,
StorageRate golden bytes, StorageRate live persistence, and
EnsureTagAsync upsert semantics).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 22:04:27 -04:00
Joseph Doherty a175c6e5a0 CLAUDE.md: mark RemoteTcp transports as live-verified
Both RemoteTcpIntegrated (full read surface + status helpers, 9 tests) and
RemoteTcpCertificate (Probe only) now pass against the host's own LAN IP,
exercising the MdasNetTcpWindows / MdasNetTcpCertificate binding branches
and SSPI/TLS handshake against a hostname rather than the loopback fast
path. True off-box verification still blocked on Windows-only
InitializeSecurityContextW in HistorianSspiClient.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 18:38:16 -04:00
Joseph Doherty b05063b195 Document the trailing 35 bytes of GetNextQueryResultBuffer rows
Investigation finding only — no behavior change, no new public fields.

Captured a fresh GetNextQueryResultBufferResponse for SysTimeSec via
instrument-wcf-readmessage and compared against the canonical 4-row
OtOpcUaParityTest_001.Counter fixture. Trailing-block structure is
tag-independent:

  bytes 0-2   constant 0x00 0x00 0x01 (sample-format marker)
  bytes 3-10  Int64 FILETIME UTC (duplicate of startTime for raw rows;
              already used by the aggregate parser as the interval start)
  bytes 11-18 zeros (likely end-time slot — populated by aggregate variants)
  bytes 19-26 varies row-to-row even with identical Quality/Value;
              looks like a storage block sequence ID or snapshot offset
  bytes 27,29 flag bytes (0/1 and 0/4 observed); semantics undecoded
  bytes 28, 30-34 zeros

None of bytes 19-34 have a clear user-facing meaning; they appear to be
server-internal storage metadata. Updated the
TryParseGetNextQueryResultBufferRows remarks block with the byte map and
a note that surfacing them as new HistorianSample fields should wait
until a customer actually asks. CLAUDE.md "Remaining gaps" entry updated
to reflect the new partial decode.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 15:35:48 -04:00
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
Joseph Doherty 15a8446f63 CLAUDE.md: correct stale "not a git working tree" note
The workspace has been a normal git working tree (origin gitea.dohertylan.com)
for some time; the safety-note line claiming otherwise was misleading. Replaced
with a one-liner pointing at the actual remote.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-04 14:53:28 -04:00
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
dohertj2 c95824a65d Initial commit: managed .NET 10 AVEVA Historian SDK + reverse-engineering toolkit
Full read-only SDK (src/AVEVA.Historian.Client) implementing the CLAUDE.md required
surface against AVEVA Historian's binary WCF protocol — no native AVEVA runtime
dependency. All operations live-verified against a local Historian:

- ProbeAsync, ReadRawAsync, ReadAggregateAsync, ReadAtTimeAsync, ReadEventsAsync
- BrowseTagNamesAsync, GetTagMetadataAsync (17 native data-type codes mapped)
- GetConnectionStatusAsync, GetStoreForwardStatusAsync, GetSystemParameterAsync
- 108/108 unit + integration tests pass

Includes the reverse-engineering toolkit (tools/AVEVA.Historian.ReverseEngineering)
used to decode the protocol: WCF probes, IL inspection via dnlib, and IL-rewrite
instrumentation (instrument-wcf-{write,read}message etc.) plus the .NET Framework
trace harness (tools/AVEVA.Historian.NativeTraceHarness) for parity testing.

Sanitized handoff evidence under docs/reverse-engineering/. Native AVEVA binaries
(current/, aveva-install-x64/, aveva-install-x86/) are gitignored — fetch separately
from the AVEVA installer.

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