Files
histsdk/docs/reverse-engineering/grpc-tag-query-odata.md
T
Joseph Doherty 85ff1b48df R0.1 browse over gRPC SHIPPED — QueryTag cracked, M0 gRPC parity complete
Wires HistorianClient.BrowseTagNamesAsync over gRPC (Transport==RemoteGrpc) via
Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync: StartTagQuery(OData) -> paged
QueryTag -> EndTagQuery. Live-verified against a real 2023 R2 server (returns Sys* tags).

QueryTag packet-id recovered WITHOUT native disassembly: a .rdata packet-descriptor
table in aahClientManaged.dll lists {0x6751,1}=StartTagQuery immediately followed by
{0x6752,1}=QueryTag (found via pefile byte-scan of .rdata), confirmed live.

Wire format (live-verified):
- request btRequest = u16 0x6752 + u16 version(1) + u16 queryType(1=names) + u32 startIndex + u32 count
- response btResonse = u32 count + per-name(u32 charCount + UTF-16LE) + trailer (NextIndex/metadata, ignored)
- new HistorianTagQueryProtocol.ParseTagNameQueryPage tolerates the trailer
- GlobToODataFilter translates the SDK glob filter to OData (Pre*->startswith, *suf->endswith,
  *mid*->contains, exact->eq); the 2023 R2 metadata-server parses filters as OData.

Replaces the earlier RE probe helpers with the shipped browse path. Adds golden-byte
(BuildQueryTagRequest) + 8 glob-translation unit tests + gated live browse test.
226 unit tests pass; 5/5 live gRPC tests pass (read, probe, system-param, metadata, browse).

Milestone 0 (full gRPC parity) is complete.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B6mcaT2PjRFKcogzp9UkfC
2026-06-21 16:01:15 -04:00

3.4 KiB

R0.1 browse over gRPC — StartTagQuery takes an OData filter (2026-06-21)

Live-probed RetrievalService.StartTagQuery / QueryTag against a real 2023 R2 server over the gRPC front door (string-handle = uppercase Open2 storage GUID). Key result: browse is feasible on 2023 R2 — the 2020 WCF "metadata-server pipe" wall does not block here.

StartTagQuery — CRACKED

StartTagQuery(strHandle, btRequest) where btRequest = the native marker(26449) + version(1) + WriteHistorianString(filter) buffer (HistorianTagQueryProtocol.CreateStartTagQueryAttempt). The server runs CMdServer::StartTagQuery::StartActiveTagnamesQuery over \\.\pipe\aahMetadataServer\console and parses the filter string as OData (not SQL-LIKE). Swept filters:

filter result
startswith(TagName,'Sys') success, 8-byte response
contains(TagName,'Sys') success
TagName eq 'SysTimeSec' success
`` (empty) success (all tags)
Sys* / * ODataFilter ... bad token
TagName like 'Sys%' / Name like 'Sys%' rejected

Success response btResponse is the 8-byte (queryHandle:uint, tagCount:uint) pair (ParseStartTagQueryResponse). Live: startswith(TagName,'Sys') → tagCount = 220.

Implication for the public API: browse must translate the SDK's glob filter to OData — * → empty, Pre*startswith(TagName,'Pre'), *sub*contains(TagName,'sub'), exact → TagName eq '...'. (Escaping single-quotes in names still TBD.)

QueryTag — CRACKED (2026-06-21), browse SHIPPED

QueryTag(strHandle, uiQueryHandle, btRequest) is the paging call that returns the tag-name rows. The blocker was the packet id: every guessed btRequest returned native error type 4 / code 72 = InvalidPacketId (ArchestrA.CloudHistorian.Contract.ErrorCode.InvalidPacketId). The generic 0x6751 header that StartTagQuery accepts is the wrong id for QueryTag.

How it was found (no Ghidra needed): a .rdata packet-descriptor table in aahClientManaged.dll lists consecutive {uint marker, uint version} entries — {0x6751, 1} (StartTagQuery) immediately followed by {0x6752, 1} (the paired op). Found by pefile byte-scan of .rdata for 51 67 00 00 and dumping the surrounding dwords. Testing 0x6752 live confirmed it.

QueryTag wire format (live-verified):

  • request btRequest = u16 marker(0x6752) + u16 version(1) + u16 queryType + u32 startIndex + u32 countqueryType = 1 returns tag-name rows (queryType = 0 returns an empty/count-only page).
  • response btResonse = u32 count + per-name(u32 charCount + UTF-16LE) + trailer (the trailer is the CloudHistorian NextIndex/TagMetadataBuffer region — ignored by HistorianTagQueryProtocol.ParseTagNameQueryPage).
  • Semantic fields match ArchestrA.CloudHistorian.Contract.QueryTagRequest (QueryType/StartIndex/TagCount; the QueryHandle travels in the protobuf uiQueryHandle).

Browse is shipped: HistorianClient.BrowseTagNamesAsync routes over gRPC when Transport==RemoteGrpc via Grpc/HistorianGrpcTagClient.BrowseTagNamesAsync (StartTagQuery(OData) → paged QueryTag(0x6752) → EndTagQuery), with the SDK glob filter translated by GlobToODataFilter. Golden-byte + glob unit tests and a gated live test (BrowseTagNamesAsync_OverGrpc_ReturnsSystemTags) cover it. M0 gRPC parity is complete.