Repository navigation
feat(antd-elixir): manifest API (REST + gRPC) - #360
Merged
Merged
Conversation
Both clients can now build and read ADR-0006 `.ant` manifests and `ant://` links through the daemon, with bang variants and Antd delegates for the REST ones: - build_manifest(client, entries, opts): entries are Antd.ManifestEntryInput structs (path, size, and exactly one of data_map or address); opts are name, torrent_info_hashes and embed_roots. Returns Antd.BuiltManifest (manifest bytes, link, entries). - decode_manifest(client, bytes, opts) and decode_manifest_link(client, link, opts), with a resolve option (default false). Both return Antd.DecodedManifest (source, name, the two torrent info hashes, entries of Antd.ManifestEntryInfo). The code sits in new files: lib/antd/manifests.ex (request building and response parsing for both transports) and lib/antd/manifest_models.ex (the four structs). Client and GrpcClient each gain one self-contained section, away from where the pointer branch adds its own. The `.ant` bytes are always a raw binary in the API. The REST client base64-encodes them in the decode request and decodes the build response's `manifest`, and a build response that is not valid base64 is an error. gRPC carries raw bytes. embed_roots defaults to true and is always sent, so a bare options list means "embed", as antd-go's zero value does. Sizes are plain integers. An unset size or exact_size is nil, never 0, on both transports: REST omits the field, and the gRPC stubs leave proto3 optional fields nil when they are absent. A recorded 0 stays 0. Every other absent value is nil too (link, path, name, data_map, error, the torrent hashes). gRPC sends these as "", and "" is truthy in Elixir, so an `if entry.error` check stays safe. An entry with both or neither of data_map and address is refused before anything is sent. So are a size that is not a u64, a non-string field and an empty decode input. Each is an Antd.BadRequestError naming entries[i], with status_code 0. The daemon's own rejections (a 400 over REST, INVALID_ARGUMENT over gRPC) map to the existing Antd.BadRequestError with the daemon's message kept. There are no new error modules. One entry's resolve failure is reported in its error field. gRPC receive limit: grpc-elixir 0.11.5 has no receive limit to raise. Its Gun adapter reads a unary response whole, GRPC.Stub.connect takes no size option, and a call raises on any option it does not know. Sends are capped only by GRPC.Message's 2 GiB default. So no option is passed, and a code comment records why. A one-off check moved 72 MiB through a mock ManifestService each way with the stock client. Each gRPC manifest function takes a :timeout option, default five minutes rather than grpc's ten seconds, since the embedding policy and resolve read the network. lib/antd/v1/manifests.pb.ex comes from scripts/generate-protos.sh with manifests added to its list (protoc-gen-elixir 0.16.0, protoc 25.3). The existing stubs regenerate byte-identically. Tests: 23 new. 11 REST tests run over Bypass and 10 gRPC tests run against a mock ManifestService. They cover: - the base64 round trip over all 256 byte values; - unset size and exact_size reading as nil next to a recorded 0; - the entry check before sending; - the embed_roots default and proto3 presence on the wire; - the 400 and INVALID_ARGUMENT mapping, and the :timeout option; - a manifest over 4 MiB received whole, and one sent whole. The full suite passes (178 tests). The modules start an unlinked GRPC.Client.Supervisor, and wait out one that is still going down with another module, after an early full run hit that race. test/antd/live_manifests_test.exs is gated on ANTD_TEST_URL and ANTD_TEST_MANIFEST_ADDRESSES, with gRPC when ANTD_TEST_GRPC is set. It pays for nothing. It passed over REST and gRPC against a 10-node devnet daemon: - a build from two public addresses (one with a size) and a private DataMap, all with paths, checking kinds, names, sizes and the link; - resolving decodes of the bytes and the link, with exact sizes matching the fixtures and the DataMap entry embedded; - a non-resolving decode, a bare address and the fixture link; - both-sources and `../x` entries, and a malformed manifest, as BadRequestError. examples/09_manifests.exs (optional ANTD_BASE_URL) ran against the same daemon. It made two public uploads, built a 184-byte manifest named "example" with a link, and decoded the link with resolve, getting exact sizes 28 and 45. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A manifest input the SDK refuses before sending is now the same error as the daemon's own refusal of it: Antd.BadRequestError with status_code 400 (it was 0), and the daemon's wording. build_manifest collects every bad entry and reports them together, as the daemon does: "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". The wording has no SDK prefix, and an empty string counts as unset. The other input checks (a size outside u64, a non-string field, a value that is not an Antd.ManifestEntryInput) stay, with the same type, status and "entries[i]: " prefix. An empty decode input (bytes or link) is "needs a manifest (base64 .ant bytes) or a link". A bad base64 manifest in a daemon response is unchanged: it is not input validation. Tests: the unit tests assert type, status and exact message, with a two-bad-entry case and a 23-entry case for the cap. The full suite passes (179 tests), with --warnings-as-errors and mix format --check-formatted clean on the touched files. The live test passed over REST and gRPC, and now checks that the client-side message equals what the daemon returns (REST minus its "Bad request: " prefix) for the same request sent past the check (REST: both-sources and neither; gRPC: neither, since a oneof cannot hold both). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Nic-dorman
marked this pull request as ready for review
October 16, 2026 09:08
Nic-dorman
changed the base branch from
nic/v2-1570-antd-manifest-endpoints
to
main
October 16, 2026 14:34
dirvine
approved these changes
Oct 17, 2026
dirvine
left a comment
Member
There was a problem hiding this comment.
Formal review: APPROVE
Repository: WithAutonomi/ant-sdk, PR #360.
Reviewed head: bc3aa2833fa5315c9c0599c236b11ce515f74ba0. Original merge base: 6c907eda10948c37cd92dd921d9dd5f68bd3e817.
No material blocker was found in the Elixir manifest API. All seven available scope/role reviews returned APPROVE, but the 7/12 panel is incomplete and is not full three-model consensus. Runtime limits are explicit.
Source and test coverage
- Complete Elixir manifest DTO/builders, shared validation, REST/gRPC adapters, tests and example against the frozen daemon manifest contract.
- Read the assertions in the REST and gRPC suites, including the in-process gRPC server cases for responses larger than 4 MiB. Exact-head GitHub checks succeeded. No mix test was run locally.
Findings and adjudication
- Both transports use shared entry validation before sending and preserve absent optional sizes rather than treating them as zero.
- REST base64 encoding and gRPC raw bytes remain transport-specific; embedding defaults and optional request fields follow the contract.
- The deliberate gRPC limit difference is documented at lib/antd/grpc_client.ex:1040-1048: this grpc-elixir version has no receive-limit option to raise. The source has real in-process-server tests for the large-message path, rather than merely asserting that an unused option was configured. Those tests were read, not executed locally.
Panel dissent and its disposition
- No substantive panel seat raised a change request for this PR. Missing seats are unavailable, not additional approval votes.
Review team: actual participation
7/12 scope/role/model seats returned substantive, receipt-validated reviews; 3/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=APPROVE, adversarial=APPROVE; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=unavailable (TimeoutError).source-2: glm: correctness=APPROVE, adversarial=unavailable (Incomplete termination; unavailable seat); qwen: correctness=APPROVE, adversarial=APPROVE; ds4: correctness=APPROVE, adversarial=APPROVE.- 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: SUCCESS=7.
- Partial panel; all three named models contributed, but five scope/role/model seats remained unavailable after retry. No full-panel consensus is claimed.
- No local Elixir build/test run or live manifest test. Author-reported mix test/devnet results remain separate from the observed GitHub check snapshot and source inspection.
- 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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
Both clients can now build and read ADR-0006
.antmanifests andant://links through the daemon, with bang variants and Antd delegatesfor the REST ones:
Antd.ManifestEntryInput structs (path, size, and exactly one of
data_map or address); opts are name, torrent_info_hashes and
embed_roots. Returns Antd.BuiltManifest (manifest bytes, link, entries).
link, opts), with a resolve option (default false). Both return
Antd.DecodedManifest (source, name, the two torrent info hashes,
entries of Antd.ManifestEntryInfo).
The code sits in new files: lib/antd/manifests.ex (request building and
response parsing for both transports) and lib/antd/manifest_models.ex
(the four structs). Client and GrpcClient each gain one self-contained
section, away from where the pointer branch adds its own.
The
.antbytes are always a raw binary in the API. The REST clientbase64-encodes them in the decode request and decodes the build
response's
manifest, and a build response that is not valid base64 isan error. gRPC carries raw bytes. embed_roots defaults to true and is
always sent, so a bare options list means "embed", as antd-go's zero
value does.
Sizes are plain integers. An unset size or exact_size is nil, never 0,
on both transports: REST omits the field, and the gRPC stubs leave
proto3 optional fields nil when they are absent. A recorded 0 stays 0.
Every other absent value is nil too (link, path, name, data_map, error,
the torrent hashes). gRPC sends these as "", and "" is truthy in
Elixir, so an
if entry.errorcheck stays safe.An entry with both or neither of data_map and address is refused before
anything is sent. So are a size that is not a u64, a non-string field and
an empty decode input. Each is an Antd.BadRequestError naming entries[i],
with status_code 0. The daemon's own rejections (a 400 over REST,
INVALID_ARGUMENT over gRPC) map to the existing Antd.BadRequestError with
the daemon's message kept. There are no new error modules. One entry's
resolve failure is reported in its error field.
gRPC receive limit: grpc-elixir 0.11.5 has no receive limit to raise. Its
Gun adapter reads a unary response whole, GRPC.Stub.connect takes no size
option, and a call raises on any option it does not know. Sends are
capped only by GRPC.Message's 2 GiB default. So no option is passed, and
a code comment records why. A one-off check moved 72 MiB through a mock
ManifestService each way with the stock client. Each gRPC manifest
function takes a :timeout option, default five minutes rather than
grpc's ten seconds, since the embedding policy and resolve read the
network.
lib/antd/v1/manifests.pb.ex comes from scripts/generate-protos.sh with
manifests added to its list (protoc-gen-elixir 0.16.0, protoc 25.3). The
existing stubs regenerate byte-identically.
Tests: 23 new. 11 REST tests run over Bypass and 10 gRPC tests run
against a mock ManifestService. They cover:
The full suite passes (178 tests). The modules start an unlinked
GRPC.Client.Supervisor, and wait out one that is still going down with
another module, after an early full run hit that race.
test/antd/live_manifests_test.exs is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES, with gRPC when ANTD_TEST_GRPC is set. It
pays for nothing. It passed over REST and gRPC against a 10-node devnet
daemon:
DataMap, all with paths, checking kinds, names, sizes and the link;
the fixtures and the DataMap entry embedded;
../xentries, and a malformed manifest, asBadRequestError.
examples/09_manifests.exs (optional ANTD_BASE_URL) ran against the same
daemon. It made two public uploads, built a 184-byte manifest named
"example" with a link, and decoded the link with resolve, getting exact
sizes 28 and 45.
Overlap with this SDK's pointer PR #330. A trial merge conflicts only in: antd-elixir/README.md, antd-elixir/scripts/generate-protos.sh. Resolve by keeping both sides when the two land.
Version bumps happen at release time; nothing here bumps a package version.
🤖 Generated with Claude Code