Skip to content

v0.3.0

Choose a tag to compare

@rustyconover rustyconover released this 17 Sep 03:02
· 24 commits to main since this release

This is a breaking release. 0.x minor bumps are this project's breaking-change
vehicle, and 22 commits since v0.2.0 include deliberate wire-level changes. Read
the migration notes below before upgrading — a v0.2.0 client will not interoperate
with a v0.3.0 peer without changes.

Nothing is published to any package registry. The only artifact is the source
tarball attached to this release; consumers pin this tag via find_package(vgi_rpc)
or a vcpkg manifest reference.

Breaking changes

__describe__ is retired — migrate to vgi_rpc.Reflection.v1

The __describe__ method no longer exists server-side. Service description is now
a first-class protocol, vgi_rpc.Reflection.v1, with two methods:

  1. list_protocols — returns the server id, server version, request version, and
    every protocol the server hosts (reflection itself included, listed first).
  2. describe — returns the full description of one named protocol.

Callers that issued a single __describe__ now make two calls: list, then describe
the protocol you want. ProtocolListing::application() picks the application
protocol out of the listing rather than assuming it is the first entry.

vgi_rpc.Reflection.v1 describes its own methods. Four ports in the family
shipped it reporting zero methods; if you consume a description of the reflection
protocol itself, you will now get a populated one.

DESCRIBE_VERSION_VALUE moved from "4" to "5" in step with the reference.
This constant never crosses the wire — it is stamped into the ServiceDescription
a client builds — so no server-role test in any port could observe the drift. If
you compare against it, update your expectation.

The vgi_rpc.protocol routing key is required

Every call must carry a vgi_rpc.protocol metadata key naming the protocol it is
addressed to. It is required on the raw transports (pipe, unix, tcp) and
optional over HTTP, where the namespaced request path already carries the same
information.

A client that omits it will be refused. This is the change that had this port's own
client calling a retired method against flat paths while every C++-only suite
reported green — see the new client-role conformance coverage below.

HTTP routes are namespaced accordingly; a client posting flat paths must move to
the namespaced form.

The protocol hash is redefined over canonical JSON

The digest is now computed over a canonical JSON encoding of the protocol
description, using canonical Arrow type tokens. Any pinned digest changes. The
primary conformance protocol moved 5cc76877… → 7713e810… (87 → 88 methods,
the new method being produce_annotated_batches).

If you assert on a protocol hash, re-read it from a running server rather than
transcribing a literal. Six ports in the family were logging six different wrong
hashes before this landed.

Note that stream_kind remains excluded from the hash by design, as does
DESCRIBE_VERSION_VALUE.

vgi_rpc.location.source now carries the full URL, including its query string

Behaviour change for anyone reading this key. This port previously redacted the
query string out of vgi_rpc.location.source. It now stamps the fetched URL in
full, matching the other six ports and WIRE_PROTOCOL.md §12.

This value is credential-bearing and must be kept out of logs. A presigned URL's
query string is a bearer credential. The redaction rule in §12 governs diagnostics,
not application metadata — the same URL already travels on the wire in
vgi_rpc.location, and a redacted value names a URL nobody fetched, which defeats
the key's purpose. redact_external_url keeps its five other call sites, all of
them error messages, which is the scope §12 gives it.

If you log resolved batch metadata, audit those sites before upgrading.

In-flight stream continuations break across the upgrade (narrow)

The call token (vgi_rpc.call_state#b64) is now sealed under an AAD that binds the
routed protocol name, not just the opening identity — this is what makes a
cross-protocol continuation fail the AEAD tag check rather than resume a cursor
under a policy that was never applied to it. A call token minted by a v0.2.0 server
will not open on a v0.3.0 server, and vice versa.

Precisely scoped:

  • Sticky session tokens are unaffected. The VGI-Session token's AAD is
    byte-identical to v0.2.0 and survives the upgrade.
  • Only deployments running with the call-state cache disabled are affected. The
    call token is opened only when call_state_cache is off — the stateless,
    load-balanced configuration. A deployment on the default warm cache never opens
    it.
  • Both roles, since this port mints as well as consumes.

The failure surfaces as a 400 ProtocolError, "Stream continuation is missing its
call token", which names the symptom rather than the version skew. Drain in-flight
streams across the upgrade, or accept that stateless continuations open before the
upgrade will not resume after it.

Additions

vgi_rpc.Identity.v1

Token-identity minting and introspection, wired end to end and driven by the shared
conformance fixture. All 77 cases in the shared identity group now run — this port
previously skipped 76 of them for want of a fixture, which is a legitimate answer to
"this port hosts no identity fixture" and an illegitimate one to "this port
implements the protocol and has never had it called".

Identity is opt-in: a worker that configures no hook hosts no identity protocol.
That is the property that keeps a dependency upgrade from growing a
credential-to-identity oracle on every existing worker. --identity {off,both,introspect-only} selects the fixture's posture; the narrowed mode
configures no mint hook, which makes method-level narrowing observable.

X-Conformance-Auth-Time joins X-Conformance-Principal as a fixture affordance,
carried into claims["auth_time"] verbatim and unparsed — the guard parses, the
transport only transports.

External-location pointer resolution in the raw native client

This port's main piece of work in this window. RpcClient::resolve_data
previously threw "external-location batches are not supported by the raw native client". It now resolves pointers on the unary, header, and stream paths.

Externalization is not an HTTP feature — WIRE_PROTOCOL.md §12 governs every
transport that carries record batches — so a byte-stream client could not read an
externalized stream at all. Both paths are covered, not one: a stream header is
externalizable and arrives as a zero-row pointer, so external pointers move into
read_substream's data arm alongside SHM pointers rather than being consumed and
dropped. A reader that classifies the header as log-or-control before testing for
vgi_rpc.location reports no header at all rather than a malformed one, which is
why four of seven ports had this defect and none saw it.

RpcClientOptions::external_http defaults to the policy HttpClient already uses
— HTTPS only, globally routable addresses, bounded redirects, separate
encoded/decoded caps. Set it to nullopt to restore the old refusal for a
deployment whose byte-stream client must make no outbound request of its own.

resolve_data now enumerates BatchType rather than defaulting, so -Wall names
any reader that has not handled a new variant.

Byte-stream externalization and produce_annotated_batches

produce_annotated_batches is where per-emit metadata meets externalization — one
port in the family refused to externalize any batch carrying metadata, using "has
metadata" as a proxy for "is a control batch"; another externalized and then
replaced the result's metadata, erasing the pointer. This port is structurally
immune to both, because OutputCollector records which entry is the data batch
rather than inferring it, but nothing proved it until a method emitted per-emit
metadata at all.

Fixes

VGI-Max-Upload-Bytes advertised a response cap

The header bounds a body a client PUTs to a vended upload URL. It was being filled
with max_externalized_response_bytes, which bounds a response the server
uploads — two caps, opposite directions, one number. Configure a tight response cap
and the server silently told every client it could not send a large request either.

max_upload_bytes is now its own HttpConfig field, appended at the end for
positional aggregate source compatibility, defaulting to the 2 GiB the old
expression produced when no response cap was configured. An unconfigured server
advertises exactly what it did before.

Nine client defects no server-role run could see

Found by driving this port's client against the Python reference server for the
first time. Every one survived a green ctest, a green run_conformance.sh, and a
green cross-port drift check, because all three drive this port's server with the
reference client.

  • Both clients deleted a caller-supplied vgi_rpc.protocol_version when the client
    itself had none configured, so a dynamic caller could not make a single call
    against any versioned protocol.
  • Producer streams could send no per-tick application metadata on either transport.
    A tick carries an empty batch, so the metadata map is the only thing a caller can
    vary per turn.
  • The HTTP client had no dynamic-schema mode: open_producer and
    open_stream_exchange demanded schemas the protocol description does not carry.
    Null now means adopt-from-the-wire-then-enforce, as the raw transports have always
    done.
  • Two exchange-decoding defects around zero-row payloads: a turn whose cursor
    arrives on a batch of its own was counted as a second data batch, and exchange
    init refused the batch emitted for an empty output schema.
  • post() decided whether to externalize an oversized request from a capability
    cache only post_inline() populates, so the first oversized request on a
    connection failed hard and the second succeeded.
  • Sticky echo-header names kept whatever casing the wire used, making the captured
    map uncomparable against a server whose stack title-cases headers.
  • RpcRemoteError composed type + ": " + message for what() while the raw
    transports carried the peer's message verbatim, so one error read ValueError: boom over a pipe and ValueError: ValueError: boom over HTTP.

Testing and CI

  • Client-role conformance. conformance/conformance_client_driver.cpp is a JSONL
    bridge the shared Python suite drives, putting this port's client on the wire
    against the reference server. That leg is the gate — a conforming client passes
    the conformance suite against the reference server, and a green run against our
    own server cannot establish that, because every accommodation a server makes for
    the client it ships with is invisible to that pair. A second leg against this
    port's own server is a regression check whose value is triage. Both run on every
    conformance platform, because what is under test is transport code.
  • Cross-port drift on every push. describe_diff compares every protocol this
    port hosts, by hash, against the in-process Python server; identity_consistency
    greps this port's sources for the vgi_rpc.Identity.v1 invariants no wire hash can
    see. The reference is cloned unpinned from main, deliberately: a reference change
    that breaks this port should turn this port red the day it lands.
  • Formatting is pinned to clang-format ~=22.1 and checked in CI ahead of the build
    matrix. Note that an older local clang-format cannot evaluate this repo's style
    and will report confidently wrong results — use the pinned version.

Full Changelog: v0.2.0...v0.3.0