Skip to content

feat(antd-cpp): manifest API (REST + gRPC) - #362

Merged
Nic-dorman merged 2 commits into
mainfrom
nic/v2-1580-manifests-cpp
Oct 19, 2026
Merged

Nic-dorman merged 2 commits into
mainfrom
nic/v2-1580-manifests-cpp

Conversation

@Nic-dorman

@Nic-dorman Nic-dorman commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Part of the manifest API fan-out. Stacked on #346 (the manifest endpoints in antd and antd-go), which sits on the pointer API #326; review those first.

Client, AsyncClient and GrpcClient gain antd's ADR-0006 manifest
surface, mirroring antd-go:

  • build_manifest(entries, options = {}) returns BuiltManifest: the
    .ant bytes, the link (empty when it would pass 1,500 bytes) and the
    entries in encoded order
  • decode_manifest(bytes, resolve = false) and
    decode_manifest_link(link, resolve = false) return DecodedManifest

The types live in a new header, antd/manifests.hpp: ManifestEntryInput
(path, size, and exactly one of data_map or address),
BuildManifestOptions, ManifestEntryInfo, BuiltManifest and
DecodedManifest. BuildManifestOptions follows antd-go's
KeepShrunkDataMaps: keep_shrunk_data_maps defaults to false, so a
default-constructed value embeds roots, and embed_roots is always sent
on both transports rather than left to the daemon's default. An entry
with both or neither source throws BadRequestError naming entries[i]
before anything is sent, as do empty bytes or an empty link.

The .ant bytes are std::vector<uint8_t> on both transports. REST
carries them as base64 in JSON (manifest in the build response and
the decode request), which the client encodes and decodes; a response
that is not valid base64 is an InternalError. gRPC carries raw bytes.

Sizes (size, exact_size) are std::optionalstd::uint64_t, as the
SDK's other sizes are u64. Unset is std::nullopt, never 0: REST reads
only a non-negative JSON integer, so an absent, null or mistyped field
is unset, and gRPC reads the proto3 optional by presence. A path or
name that is absent is std::nullopt too. A recorded 0 stays 0.

C++ gRPC has no per-call message limit, so the manifest calls get their
own channel to the same target, made with grpc::ChannelArguments
SetMaxReceiveMessageSize and SetMaxSendMessageSize at 256 MiB (antd-go's
maxManifestGrpcMessage). A manifest can be 64 MiB and a Build or Decode
message carries it; every other service keeps the default channel and
its 4 MiB receive limit. Like the default channel, it connects on first
use.

No new error types. The daemon's 400 BAD_REQUEST and gRPC
INVALID_ARGUMENT are BadRequestError with the daemon's message, which
names the entry. One entry's resolve failure stays in that entry's
error, not a failed call. Nothing here pays, and the README says so.

To stay clear of the pointer branch, the wire mapping is in new files
(src/manifests_json.cpp, src/manifests_grpc.cpp), and the shared files
get small, self-contained blocks away from the pointer hunks. A trial
merge with the pointer branch is clean, and the merged tree builds and
passes all 12 test binaries.

Tests:

  • test_manifests.cpp: 13 cases against a stub REST daemon, on Client
    and AsyncClient. They cover the base64 round trip (all 256 byte
    values and each padding length), unset and mistyped sizes as nullopt
    next to a recorded 0, the both-or-neither check with no request
    sent, the embed_roots default and omitted options, and a 400 as
    BadRequestError.
  • test_grpc_manifests.cpp: 9 cases against a mock grpc++
    ManifestService. They cover the same mapping by proto3 presence and
    INVALID_ARGUMENT. They also check the channel arguments by
    inspection, and send a 5 MiB manifest both ways through the client,
    which a default channel refuses with RESOURCE_EXHAUSTED.
  • tests/live_manifests.cpp is gated on ANTD_TEST_URL and
    ANTD_TEST_MANIFEST_ADDRESSES, plus ANTD_TEST_GRPC in a gRPC build.
    It makes no paid calls. It passed against a 10-node devnet over REST
    and gRPC (155 assertions):
    • a build of two public addresses and a private DataMap, all with
      paths
    • resolving decodes of the bytes and of the link, with exact sizes
      matching the fixtures and the DataMap entry embedded with its
      DataMap
    • the fixture link
    • a client-side ambiguous entry, and the daemon's 400 for "../x"
    • identical bytes from both transports

examples/09-manifests.cpp (optional ANTD_BASE_URL) ran against the same
daemon. It uploaded two notes, built a 184-byte manifest with a link,
and resolved exact sizes of 31 and 48 bytes. Built with g++ 13 and
CMake 3.28, warning-free under -Wall -Wextra -Wpedantic -Wshadow (bar
the existing errors.hpp and client.cpp warnings). For gRPC it used the
apt protoc 3.21.12 with grpc++ 1.51. The stubs are generated at build
time, so none are committed.

Overlap with this SDK's pointer PR #327: a trial merge is clean.

Version bumps happen at release time; nothing here bumps a package version.

🤖 Generated with Claude Code

Nic-dorman and others added 2 commits October 15, 2026 14:31
Client, AsyncClient and GrpcClient gain antd's ADR-0006 manifest
surface, mirroring antd-go:
- build_manifest(entries, options = {}) returns BuiltManifest: the
  `.ant` bytes, the link (empty when it would pass 1,500 bytes) and the
  entries in encoded order
- decode_manifest(bytes, resolve = false) and
  decode_manifest_link(link, resolve = false) return DecodedManifest

The types live in a new header, antd/manifests.hpp: ManifestEntryInput
(path, size, and exactly one of data_map or address),
BuildManifestOptions, ManifestEntryInfo, BuiltManifest and
DecodedManifest. BuildManifestOptions follows antd-go's
KeepShrunkDataMaps: keep_shrunk_data_maps defaults to false, so a
default-constructed value embeds roots, and embed_roots is always sent
on both transports rather than left to the daemon's default. An entry
with both or neither source throws BadRequestError naming `entries[i]`
before anything is sent, as do empty bytes or an empty link.

The `.ant` bytes are std::vector<uint8_t> on both transports. REST
carries them as base64 in JSON (`manifest` in the build response and
the decode request), which the client encodes and decodes; a response
that is not valid base64 is an InternalError. gRPC carries raw bytes.

Sizes (`size`, `exact_size`) are std::optional<std::uint64_t>, as the
SDK's other sizes are u64. Unset is std::nullopt, never 0: REST reads
only a non-negative JSON integer, so an absent, null or mistyped field
is unset, and gRPC reads the proto3 `optional` by presence. A path or
name that is absent is std::nullopt too. A recorded 0 stays 0.

C++ gRPC has no per-call message limit, so the manifest calls get their
own channel to the same target, made with grpc::ChannelArguments
SetMaxReceiveMessageSize and SetMaxSendMessageSize at 256 MiB (antd-go's
maxManifestGrpcMessage). A manifest can be 64 MiB and a Build or Decode
message carries it; every other service keeps the default channel and
its 4 MiB receive limit. Like the default channel, it connects on first
use.

No new error types. The daemon's 400 BAD_REQUEST and gRPC
INVALID_ARGUMENT are BadRequestError with the daemon's message, which
names the entry. One entry's resolve failure stays in that entry's
`error`, not a failed call. Nothing here pays, and the README says so.

To stay clear of the pointer branch, the wire mapping is in new files
(src/manifests_json.cpp, src/manifests_grpc.cpp), and the shared files
get small, self-contained blocks away from the pointer hunks. A trial
merge with the pointer branch is clean, and the merged tree builds and
passes all 12 test binaries.

Tests:
- test_manifests.cpp: 13 cases against a stub REST daemon, on Client
  and AsyncClient. They cover the base64 round trip (all 256 byte
  values and each padding length), unset and mistyped sizes as nullopt
  next to a recorded 0, the both-or-neither check with no request
  sent, the embed_roots default and omitted options, and a 400 as
  BadRequestError.
- test_grpc_manifests.cpp: 9 cases against a mock grpc++
  ManifestService. They cover the same mapping by proto3 presence and
  INVALID_ARGUMENT. They also check the channel arguments by
  inspection, and send a 5 MiB manifest both ways through the client,
  which a default channel refuses with RESOURCE_EXHAUSTED.
- tests/live_manifests.cpp is gated on ANTD_TEST_URL and
  ANTD_TEST_MANIFEST_ADDRESSES, plus ANTD_TEST_GRPC in a gRPC build.
  It makes no paid calls. It passed against a 10-node devnet over REST
  and gRPC (155 assertions):
  - a build of two public addresses and a private DataMap, all with
    paths
  - resolving decodes of the bytes and of the link, with exact sizes
    matching the fixtures and the DataMap entry embedded with its
    DataMap
  - the fixture link
  - a client-side ambiguous entry, and the daemon's 400 for "../x"
  - identical bytes from both transports

examples/09-manifests.cpp (optional ANTD_BASE_URL) ran against the same
daemon. It uploaded two notes, built a 184-byte manifest with a link,
and resolved exact sizes of 31 and 48 bytes. Built with g++ 13 and
CMake 3.28, warning-free under -Wall -Wextra -Wpedantic -Wshadow (bar
the existing errors.hpp and client.cpp warnings). For gRPC it used the
apt protoc 3.21.12 with grpc++ 1.51. The stubs are generated at build
time, so none are committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mistake the client refuses before sending is now indistinguishable
from the daemon's 400: the same BadRequestError (status 400) and the
daemon's exact text.

build_manifest collects every entry with both or neither of data_map
and address (an empty string counts as unset) and throws one error,
worded as the daemon's parse_entries and entry_errors_message do:
"{n} invalid entries: entries[i]: give data_map or address, not both;
entries[j]: needs a data_map or an address", at most 20 shown, then
"; and {k} more". An empty decode input (bytes or link) throws "needs a
manifest (base64 .ant bytes) or a link". The message is the bare text,
as gRPC sends it, with no SDK prefix. Response parsing failures (bad
base64) stay InternalError.

Tests assert the type, status and exact message on REST and gRPC, with
a case of two bad entries and one past the 20-entry cap. The live test
checks the client-side message against the daemon's answer to the same
request sent past the client check. REST 13 cases and gRPC 9 pass, and
the live test passes over both transports (163 assertions).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman marked this pull request as ready for review October 16, 2026 09:08
@Nic-dorman
Nic-dorman changed the base branch from nic/v2-1570-antd-manifest-endpoints to main October 16, 2026 14:34

@dirvine dirvine left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Formal review: APPROVE

Repository: WithAutonomi/ant-sdk, PR #362.
Reviewed head: 11ccb7de4d3d90b6833e3d6c61b1db2a5d88efc6. Original merge base: 6c907eda10948c37cd92dd921d9dd5f68bd3e817.

No material blocker remains in the C++ manifest API after resolving evidence-fragment objections against the complete source and building this exact checkout. The partial panel returned 5/12 substantive reviews (2 APPROVE, 3 CHANGES_REQUIRED) from Qwen and DS4; GLM remained unavailable after retry. The formal verdict follows the source/build adjudication below, not a majority-vote count.

Source and test coverage

  • Complete C++ manifest models, REST/gRPC adapters, shared validation, channel arguments, test sources and example against the frozen daemon/protobuf contract.
  • Parent configured and built this exact #362 checkout with ANTD_BUILD_GRPC=OFF, including examples and test binaries. Reused local dependency sources only after verifying their tags matched the declared pins; FetchContent was fully disconnected. No dependency install or fetch was needed.
  • Parent ran ctest --output-on-failure -E live on that new build: 5/5 offline test binaries passed, exit 0, including antd_manifest_tests. ANTD_TEST* flags were removed. Live test binaries were compiled but excluded from execution. This is not #327's build receipt and does not exercise the gRPC transport.

Findings and adjudication

  • The supposedly absent manifests_json.cpp and manifests_grpc.cpp exist at the frozen head. They validate entries before transport (src/manifests_json.cpp:65; src/manifests_grpc.cpp:45). These were omitted from individual scope fragments, not from the implementation.
  • REST parses an unsigned numeric size into an optional, preserving a present zero instead of conflating it with absence (manifests_json.cpp:33-40). It always sends embed_roots and correctly converts manifest bytes to/from base64; malformed base64 fails closed (:82,104-107).
  • detail::manifest_channel_arguments is defined, not merely declared. Both gRPC send and receive limits are 256 MiB (src/manifests_grpc.cpp:36-41; include/antd/grpc_client.hpp:21). Both plaintext and TLS constructors use those arguments. The corresponding mock-server tests were read, not run.
  • Shared entry validation is present for both transports, and the new REST build plus manifest test binary execute successfully at this head.

Panel dissent and its disposition

  • The three CHANGES_REQUIRED reports depended on implementations being missing from their evidence fragment: shared JSON validation/parsing or the gRPC channel-argument function. Both complete-source files were inspected; the REST implementation was also compiled and tested. These claims do not establish a remaining defect. The original votes remain recorded below; this is parent adjudication, not retrospective panel unanimity.

Review team: actual participation

5/12 scope/role/model seats returned substantive, receipt-validated reviews; 2/3 distinct named models. Partial panel; provisional coverage limits apply to this formal verdict. A receipt is evidence of participation, not proof that its findings are correct. Counts below are per evidence scope, not additional independent models.

  • source-1: glm: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=unavailable (Incomplete termination; unavailable seat); qwen: correctness=CHANGES_REQUIRED, adversarial=CHANGES_REQUIRED; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=unavailable (Incomplete termination; unavailable seat).
  • source-2: glm: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=unavailable (Incomplete termination; unavailable seat); qwen: correctness=APPROVE, adversarial=CHANGES_REQUIRED; ds4: correctness=APPROVE, adversarial=unavailable (Incomplete termination; unavailable seat).
  • Models: GLM 5.3 / OpenRouter z-ai/glm-5.3; local Qwen 3.8 / qwen38-27b; local DS4 Flash / deepseek-v4-flash. Missing seats received the configured same-model retry; no replacement model is counted as an independent vote.

Publication gates and limits

  • Live head rechecked against the reviewed head. Live base ea389c79c2ac40601c574e93031a66a9791db1d2: binary diff matches the reviewed diff, merge base is unchanged, no intersecting base-drift paths, and trial merge exited 0.
  • GitHub check snapshot: no checks reported; this is not a passing CI result.
  • Only Qwen and DS4 returned valid seats. GLM remained unavailable after the bounded retry; this is a 5/12 partial panel, not three-model consensus.
  • No GitHub checks are reported for this head. The successful local REST-only build/tests are not a substitute claim of green CI.
  • The gRPC transport was not built or run. The large-message gRPC server tests and live manifest tests were not executed. Author-reported devnet results are not counted as observed evidence.
  • No paid or live-network tests. No merge, deployment, source change, or Linear state change. This verdict applies only to the exact head above; it is not a fleet-wide or cross-SDK integration sign-off.

@Nic-dorman
Nic-dorman merged commit ae06c5c into main Oct 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants