From 2246fdd395277224b9d0cdf414681dc0a9966727 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 20 Jun 2026 15:22:53 -0400 Subject: [PATCH 1/2] docs: reclassify M1a R1.1/R1.3 as blocked on 2020 WCF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Live-probed both against the local Historian 2020 (WCF): - R1.3 GetServerTimeZoneAsync: Status.GetSystemTimeZoneName returns rc=0 with an empty value under a real authenticated handle โ€” a client-side stub in the GetServerTime family. gRPC/2023R2-only. Reverted the implementation. - R1.1 ExecuteSqlCommandAsync: Retrieval.ExeC returns native error type 4 / code 51 (InvalidParameter); the contract-3 string-handle ops require an unmapped native session/filter registration step (the StartTagQuery wall). Adds an M1a re-classification note steering future work toward proven uint-handle / already-wired ops (R1.4 GETHI next) over string-handle ops. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/plans/hcal-roadmap.md | 18 ++++++++++++++++-- .../wcf-status-localhost.md | 16 ++++++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/docs/plans/hcal-roadmap.md b/docs/plans/hcal-roadmap.md index 53432b3..9ac0edc 100644 --- a/docs/plans/hcal-roadmap.md +++ b/docs/plans/hcal-roadmap.md @@ -24,6 +24,20 @@ HCAL replacement, built on the **2023 R2 gRPC transport**. Derived from > golden-byte/unit-tested here but **cannot be live-verified** without an actual 2023 R2 server. > Treat gRPC ops as unverified until then; the byte payloads remain the proven 2020 protocol. +> ๐Ÿ”ฌ **M1a re-classification (2026-06-20).** Two "trivial" items were live-probed against the +> 2020 WCF server and found **not deliverable here**, both for evidence-backed reasons: +> - **R1.3 `GetServerTimeZoneAsync`** โ€” `Status.GetSystemTimeZoneName` is a client-side *stub* +> on 2020 (rc=0, empty value), same family as `GetServerTime`. gRPC/2023R2-only. +> - **R1.1 `ExecuteSqlCommandAsync`** โ€” `ExeC` returns native error 51 (InvalidParameter); +> the contract-3 string-handle ops require an unmapped native session/filter registration +> step (the `StartTagQuery` wall). +> +> Takeaway: the M1a "cheap surface" is *cheap only on the 2023 R2 gRPC front door*. On 2020 WCF, +> the genuinely reachable next items are those that reuse a **proven uint-handle Retrieval op or +> an already-wired call** โ€” e.g. **R1.4 `GetHistorianInfoAsync`** (GETHI is already invoked in +> the event chain) and the extended/localized-property reads (R1.5/R1.6) that ride +> `GetTagInfo*`-style ops. Prefer those before any string-handle (`ExeC`/`QTB`/`QTG`) op. + ## Guiding principles 1. **gRPC-first.** New ops go on the `RemoteGrpc` transport (clean protobuf envelope); @@ -68,9 +82,9 @@ read/browse/status surface is Windows-free and the gRPC stack is the default pat ### 1a. Trivial (XSโ€“S each, no new payload format) | ID | Capability | gRPC op | Notes | |---|---|---|---| -| R1.1 | `ExecuteSqlCommandAsync` | `Retrieval.ExecuteSqlCommand` | string in โ†’ `iRetValue` + status; thin | +| ~~R1.1~~ | ~~`ExecuteSqlCommandAsync`~~ | `Retrieval.ExecuteSqlCommand` | โš  **Blocked on 2020 WCF.** Live-probed 2026-06-20: `ExeC` returns native error type 4 / code **51 (InvalidParameter)** for every handle variant โ€” same unmapped *native session/filter registration* prerequisite that blocks `StartTagQuery`/`QueryTag` (see `implementation-status.md` lines ~982, ~1404). Needs that registration RE'd, or a 2023 R2 gRPC server. Do not wire via guessed calls. | | R1.2 | `GetRuntimeParameterAsync` | `Status.GetRuntimeParameter` | mirror `GetSystemParameter` | -| R1.3 | `GetServerTimeZoneAsync` | `Status.GetSystemTimeZoneName` | string out | +| ~~R1.3~~ | ~~`GetServerTimeZoneAsync`~~ | `Status.GetSystemTimeZoneName` | โš  **gRPC/2023R2-only.** Verified 2026-06-20: over **2020 WCF** this op is a stub (rc=0, empty value) in the `GetServerTime` family โ€” not shippable here. Build+verify only against a live 2023 R2 server. See `docs/reverse-engineering/wcf-status-localhost.md`. | ### 1b. Bounded (decode one `bytes` payload; Sโ€“M each) | ID | Capability | gRPC op | Payload to decode | Depends | diff --git a/docs/reverse-engineering/wcf-status-localhost.md b/docs/reverse-engineering/wcf-status-localhost.md index f2a4852..d0bd776 100644 --- a/docs/reverse-engineering/wcf-status-localhost.md +++ b/docs/reverse-engineering/wcf-status-localhost.md @@ -26,9 +26,25 @@ Observed sanitized localhost results: - `GetSystemParameter(handle: 0, "Version")` returns `false` with no error buffer. +Re-tested 2026-06-20 with a **real authenticated client handle** (full Open2 auth +chain), not `handle: 0`: + +- `GetSystemParameter(handle, "HistorianVersion")` โ†’ real version string (works; + shipped as `GetSystemParameterAsync`). +- `GetSystemTimeZoneName(handle)` โ†’ return code `0x00000000` (success) but an + **empty value string**. Same channel/handle that makes `GetSystemParameter` + return real data, so this is the op's own behavior, not an auth/marshalling + gap. `GetSystemTimeZoneName` is a member of the `GetServerTime` stub family: + the 2020 WCF path returns success without producing a value (the native client + computes the zone locally). It only becomes a real round-trip on the 2023 R2 + gRPC front door (`Status.GetSystemTimeZoneName`), which is absent on this box. + Interpretation: - `Stat` endpoint routing is confirmed, but status operations that require a real client handle are not usable until managed session open is solved. - `GetServerTime` should not be promoted into the public SDK as a real server time call from this WCF path; native evidence shows it is a no-op stub here. +- **`GetServerTimeZoneAsync` (roadmap R1.3) is NOT a trivial WCF op on 2020** โ€” it + is a stub returning empty. Do not ship it over the 2020 WCF transport. Deliver + it only against a live 2023 R2 gRPC server. Reclassified in `docs/plans/hcal-roadmap.md`. From 84ec175f768fd03c2d4c18399e97e5a82210b089 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sat, 20 Jun 2026 15:32:15 -0400 Subject: [PATCH 2/2] docs: map the 2020 WCF string-handle wall (R1.4 GETHI blocked) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Probed R1.4 GetHistorianInfo (GETHI) live against the local 2020 server. GETHI returns native error type 4 / code 1 for the exact native request shape across 5 handle formats (storage GUID, context GUID, uint decimal/X8/0x-hex) even with Stat.GetV ร—2 priming. Its result is discarded (TryRun) in the only place it's used, so it was never actually verified to return data managed-side. This confirms a structural boundary on the 2020 WCF surface: ops taking a uint client handle work (the proven read/browse/metadata/status/event surface); ops taking a string GUID handle (ExeC, QTB, QTG, GETHI, GetTepByNm, ...) are blocked behind an unmapped native session/filter registration. Every remaining M1 *read* item (R1.1/R1.4/R1.5/R1.6) is string-handle -> all gated on that one RE target. Reachable uint-handle items: R1.7 event filters, R1.8/R1.9 summary modes. New: docs/reverse-engineering/wcf-string-handle-wall.md (full dichotomy + table). Roadmap R1.4/R1.5/R1.6 struck through; reachable items re-pointed. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/plans/hcal-roadmap.md | 34 +++++++---- .../wcf-string-handle-wall.md | 58 +++++++++++++++++++ 2 files changed, 81 insertions(+), 11 deletions(-) create mode 100644 docs/reverse-engineering/wcf-string-handle-wall.md diff --git a/docs/plans/hcal-roadmap.md b/docs/plans/hcal-roadmap.md index 9ac0edc..3a5c1eb 100644 --- a/docs/plans/hcal-roadmap.md +++ b/docs/plans/hcal-roadmap.md @@ -32,11 +32,14 @@ HCAL replacement, built on the **2023 R2 gRPC transport**. Derived from > the contract-3 string-handle ops require an unmapped native session/filter registration > step (the `StartTagQuery` wall). > -> Takeaway: the M1a "cheap surface" is *cheap only on the 2023 R2 gRPC front door*. On 2020 WCF, -> the genuinely reachable next items are those that reuse a **proven uint-handle Retrieval op or -> an already-wired call** โ€” e.g. **R1.4 `GetHistorianInfoAsync`** (GETHI is already invoked in -> the event chain) and the extended/localized-property reads (R1.5/R1.6) that ride -> `GetTagInfo*`-style ops. Prefer those before any string-handle (`ExeC`/`QTB`/`QTG`) op. +> Takeaway: the M1a "cheap surface" is *cheap only on the 2023 R2 gRPC front door*. On 2020 WCF +> the boundary is the **handle type** (see the string-handle wall note under ยง1b and +> `docs/reverse-engineering/wcf-string-handle-wall.md`): **`uint`-handle ops work, `string`-handle +> ops are blocked.** GETHI/GetTepByNm were probed and confirmed blocked (not, as first guessed, +> reachable). The genuinely reachable next items on 2020 WCF are the remaining **`uint`-handle** +> ops: **R1.8/R1.9 StartQuery summary/state modes** and **R1.7 event filters** (filter bytes ride +> the proven `uint`-handle `StartEventQuery`). Everything string-handle waits on one RE target: +> the native session/filter registration. ## Guiding principles @@ -86,15 +89,24 @@ read/browse/status surface is Windows-free and the gRPC stack is the default pat | R1.2 | `GetRuntimeParameterAsync` | `Status.GetRuntimeParameter` | mirror `GetSystemParameter` | | ~~R1.3~~ | ~~`GetServerTimeZoneAsync`~~ | `Status.GetSystemTimeZoneName` | โš  **gRPC/2023R2-only.** Verified 2026-06-20: over **2020 WCF** this op is a stub (rc=0, empty value) in the `GetServerTime` family โ€” not shippable here. Build+verify only against a live 2023 R2 server. See `docs/reverse-engineering/wcf-status-localhost.md`. | +> โ›” **String-handle wall (2026-06-20).** R1.4/R1.5/R1.6 (and R1.1) are **all blocked on 2020 +> WCF** for the *same* reason: their ops take a **`string` GUID handle** and require an unmapped +> native session/filter registration. Probed live โ€” GETHI returns code 1 for the exact native +> request shape across 5 handle formats + Stat.GetV priming; ExeC returns code 51. The proven +> surface uses **`uint`-handle** ops only. **One RE target โ€” the native string-handle session +> registration โ€” unblocks this whole sub-milestone.** Full analysis: +> `docs/reverse-engineering/wcf-string-handle-wall.md`. R1.8/R1.9 (StartQuery summary/state modes) +> are `uint`-handle and remain reachable on 2020 WCF. + ### 1b. Bounded (decode one `bytes` payload; Sโ€“M each) | ID | Capability | gRPC op | Payload to decode | Depends | |---|---|---|---|---| -| R1.4 | `GetHistorianInfoAsync` | `Status.GetHistorianInfo` | GETHI buffer (partly decoded; incl. `EventStorageMode`@514) | R0.5 | -| R1.5 | Extended-property **read** | `Retrieval.GetTagExtendedPropertiesFromName` | TEP result buffer | R0.5 | -| R1.6 | Localized-property **read** | `Retrieval.GetTagLocalizedPropertiesFromName` | localized buffer | R0.5 | -| R1.7 | Event **filters** | filter bytes in `Retrieval.StartEventQuery` | filter predicate encoding (name/op/value) | R0.5 | -| R1.8 | Analog-summary query | `Retrieval.StartQuery` (summary mode) | summary row layout | โ€” | -| R1.9 | State-summary query | `Retrieval.StartQuery` (state mode) | state-summary row layout | โ€” | +| ~~R1.4~~ | `GetHistorianInfoAsync` | `Status.GetHistorianInfo` | โ›” **string-handle wall** โ€” GETHI returns code 1 on 2020 WCF (all handle/priming variants). GETHI buffer incl. `EventStorageMode`@514. | string-handle RE | +| ~~R1.5~~ | Extended-property **read** | `Retrieval.GetTagExtendedPropertiesFromName` | โ›” **string-handle wall** (GetTepByNm takes `string handle`). TEP result buffer. | string-handle RE | +| ~~R1.6~~ | Localized-property **read** | `Retrieval.GetTagLocalizedPropertiesFromName` | โ›” **string-handle wall** (same family). | string-handle RE | +| R1.7 | Event **filters** | filter bytes in `Retrieval.StartEventQuery` | filter predicate encoding (name/op/value) โ€” **`uint`-handle**, reachable | R0.5 | +| R1.8 | Analog-summary query | `Retrieval.StartQuery` (summary mode) | summary row layout โ€” **`uint`-handle**, reachable | โ€” | +| R1.9 | State-summary query | `Retrieval.StartQuery` (state mode) | state-summary row layout โ€” **`uint`-handle**, reachable | โ€” | ### 1c. Bounded config writes (Sโ€“M each) | ID | Capability | gRPC op | Payload | Notes | diff --git a/docs/reverse-engineering/wcf-string-handle-wall.md b/docs/reverse-engineering/wcf-string-handle-wall.md new file mode 100644 index 0000000..63be01f --- /dev/null +++ b/docs/reverse-engineering/wcf-string-handle-wall.md @@ -0,0 +1,58 @@ +# The 2020 WCF string-handle wall (2026-06-20) + +Live-probing the local **Historian 2020** (WCF, port 32568) for HCAL roadmap M1 +surfaced a clean structural boundary on what the pure-managed client can call. It +explains why R1.1/R1.4/R1.5 all fail and identifies the single RE target that +unblocks the rest of the M1 read surface. + +## The dichotomy + +Retrieval/Status/History ops split by the **type of their first (handle) parameter**: + +| Handle type | Examples | Status on 2020 WCF | +|---|---|---| +| **`uint` client handle** (Open2 output) | `StartQuery2`, `GetNextQueryResultBuffer2`, `IsOriginalAllowed`, `GetTagInfosFromName`/`GetTagInfoFromName` (GetTgByNm), `GetSystemParameter`, `StartEventQuery`, `GetNextEventQueryResultBuffer`, `RegisterTags2`, `EnsureTags2`, `UpdateClientStatus3` | โœ… **work** โ€” the proven read/browse/metadata/status-param/event/write surface | +| **`string` GUID handle** | `ExecuteSqlCommand` (ExeC), `StartTagQuery` (QTB), `QueryTag` (QTG), `GetHistorianInfo` (GETHI), `GetTagExtendedPropertiesFromName` (GetTepByNm), `GetTagInfosFromName2` (GetTgByNm2), `GetTagidsByTagnameAndSource` | โ›” **blocked** โ€” native error type 4, code **51 (InvalidParameter)** or **1 (Failure)** | + +## Evidence (this probe + prior notes) + +- **ExeC** โ†’ type 4 / code 51 for every handle variant (storageGuid, contextGuid). + Matches `implementation-status.md` ~982 / ~1404 ("StartTagQuery depends on earlier + native session/filter registration โ€ฆ do not wire through guessed calls"). +- **GETHI** (`HistorianVersion` param query โ€” the *exact* native request shape from + `BuildGetHistorianInfoRequest`, with `Stat.GetV ร—2` priming) โ†’ type 4 / code **1** + for all five handle formats tried: storage-session GUID, context GUID, uint as + decimal, uint as `X8` hex, uint as `0x`-hex. In the only place GETHI is used (the + event-priming chain) its result is wrapped in `TryRun` and **discarded**, so there + was never evidence it actually returns data from the managed client. +- **GetTepByNm / QTB / QTG / GetTgByNm2** all take a `string handle` โ†’ same family. + +## Why + +The string-handle ops are keyed off a **native-side session/filter registration** +that the C++ client performs but the managed replay does not reproduce. The uint +client handle is the Open2 session token the server already trusts; the string GUID +handle indexes a *different* per-service registration table that stays empty unless +the native priming is replicated faithfully. `Stat.GetV ร—2` alone is insufficient. + +## Consequence for the roadmap + +Every remaining **M1 read** item is a string-handle op: + +- R1.1 `ExecuteSqlCommandAsync` (ExeC) โ€” blocked +- R1.4 `GetHistorianInfoAsync` (GETHI) โ€” blocked +- R1.5 extended-property read (GetTepByNm) โ€” blocked (string handle, confirmed) +- R1.6 localized-property read โ€” same family + +So **M1 read-surface completion on 2020 WCF is gated entirely behind one RE target: +the native session/filter registration for string-handle ops.** Reverse-engineer it +once and the whole family unlocks. Until then, the alternatives are: + +1. **RE the registration** โ€” instrument the native `CRetrievalConnectionWCF` / + `CStatusConnectionWCF` priming between Open2 and the first successful string-handle + call (capture-tier; the highest-leverage single RE task for M1). +2. **2023 R2 gRPC server** โ€” these ops are first-class on the gRPC front door, where + the handle/envelope differs and the registration wall may not apply. + +Do **not** ship any string-handle op via guessed calls (project discipline: +"leave them throwing until evidence supports an implementation").