Skip to content

feat(antd-ruby): manifest API (REST + gRPC) - #351

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

Nic-dorman merged 3 commits into
mainfrom
nic/v2-1580-manifests-ruby

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.

Antd::Client and Antd::GrpcClient can now build and read ADR-0006 .ant
manifests and ant:// links through the daemon:

  • build_manifest(entries, name:, torrent_info_hashes:, embed_roots: true)
  • decode_manifest(bytes, resolve: false)
  • decode_manifest_link(link, resolve: false)

The models are keyword structs: ManifestEntryInput (a Hash with the same
keys works too), ManifestEntryInfo, BuiltManifest and DecodedManifest, plus
ManifestEntryKind and ManifestSource string constants. The code lives in
two new files, lib/antd/manifests.rb (models and REST) and
lib/antd/grpc_manifests.rb, which reopen the client classes and are
required from lib/antd.rb, so client.rb, grpc_client.rb and models.rb are
untouched. Nothing is uploaded or paid for, and the README says so.

The API always takes and returns raw .ant bytes (a binary String). REST
carries them as base64 in manifest; a build response that is not valid
base64 raises InternalError. gRPC sends them as-is.

Sizes are Integers. Anything unset reads as nil on both transports: REST's
omitted fields, and over gRPC the proto3 optionals (checked with has_size?,
has_exact_size?, has_path?, has_name?) and empty strings. A missing size
or exact_size is nil, never 0, and a recorded 0 stays 0. The link,
data_map, error and torrent info hashes are nil when absent, as the
pointer models' unset fields are. embed_roots defaults to true and is
always sent, false matching antd-go's KeepShrunkDataMaps.

Before anything is sent, an entry with both or neither of data_map and
address (nil and "" count as unset), a size that is not a non-negative
Integer, or an empty manifest or link raises ArgumentError, as antd-go's
checkManifestEntries does. An entry the daemon rejects is its 400
(gRPC INVALID_ARGUMENT) as the existing BadRequestError, keeping the
daemon's message naming entries[i]. One entry's resolve failure is in its
error, not a failed call. There are no new error types.

grpc-ruby has no per-call message limit, so ManifestService gets a stub on
a channel of its own, made on first use, with grpc.max_receive_message_length
and grpc.max_send_message_length at 256 MiB (MANIFEST_GRPC_MAX_MESSAGE,
antd-go's ceiling). The other services keep gRPC's defaults.

Tests: 22 unit tests (12 REST over webmock, 10 gRPC against a fake
ManifestService stub answering with real proto messages) cover the base64
round trip, unset sizes as nil, the both-or-neither check, the embed_roots
default and presence, and the 400 / INVALID_ARGUMENT mapping. Two of them
check the gRPC limit: the channel args the manifest stub is built with,
and an in-process server sending a 5 MiB manifest that a default stub
refuses (RESOURCE_EXHAUSTED) and the client receives and decodes. The full
suite passes (151 runs, the two live tests skipped).

test/test_live_manifests.rb is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES (ANTD_TEST_GRPC adds the gRPC run). Against a
10-node devnet it passed over REST and gRPC (118 assertions): a build from
two public addresses and a private DataMap, with paths; decoding the bytes
and the link with resolve, exact sizes matching the fixtures and the
DataMap entry embedded; a bare address as a file link; the fixture
manifest link; and a bad path as BadRequestError. It pays nothing.

examples/09_manifests.rb (ANTD_BASE_URL) stored two pieces of public data,
built a 184-byte manifest with its link, and decoded the link with resolve
(26 and 43 bytes) against the same daemon.

Stubs regenerated with grpc-tools 1.84.0, the version the script pins;
the existing stubs regenerate byte-identically.

Overlap with this SDK's pointer PR #336. A trial merge conflicts only in: antd-ruby/README.md, antd-ruby/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

Nic-dorman and others added 2 commits October 15, 2026 14:14
Antd::Client and Antd::GrpcClient can now build and read ADR-0006 `.ant`
manifests and `ant://` links through the daemon:
- build_manifest(entries, name:, torrent_info_hashes:, embed_roots: true)
- decode_manifest(bytes, resolve: false)
- decode_manifest_link(link, resolve: false)

The models are keyword structs: ManifestEntryInput (a Hash with the same
keys works too), ManifestEntryInfo, BuiltManifest and DecodedManifest, plus
ManifestEntryKind and ManifestSource string constants. The code lives in
two new files, lib/antd/manifests.rb (models and REST) and
lib/antd/grpc_manifests.rb, which reopen the client classes and are
required from lib/antd.rb, so client.rb, grpc_client.rb and models.rb are
untouched. Nothing is uploaded or paid for, and the README says so.

The API always takes and returns raw `.ant` bytes (a binary String). REST
carries them as base64 in `manifest`; a build response that is not valid
base64 raises InternalError. gRPC sends them as-is.

Sizes are Integers. Anything unset reads as nil on both transports: REST's
omitted fields, and over gRPC the proto3 optionals (checked with has_size?,
has_exact_size?, has_path?, has_name?) and empty strings. A missing size
or exact_size is nil, never 0, and a recorded 0 stays 0. The link,
data_map, error and torrent info hashes are nil when absent, as the
pointer models' unset fields are. embed_roots defaults to true and is
always sent, false matching antd-go's KeepShrunkDataMaps.

Before anything is sent, an entry with both or neither of data_map and
address (nil and "" count as unset), a size that is not a non-negative
Integer, or an empty manifest or link raises ArgumentError, as antd-go's
checkManifestEntries does. An entry the daemon rejects is its 400
(gRPC INVALID_ARGUMENT) as the existing BadRequestError, keeping the
daemon's message naming entries[i]. One entry's resolve failure is in its
error, not a failed call. There are no new error types.

grpc-ruby has no per-call message limit, so ManifestService gets a stub on
a channel of its own, made on first use, with grpc.max_receive_message_length
and grpc.max_send_message_length at 256 MiB (MANIFEST_GRPC_MAX_MESSAGE,
antd-go's ceiling). The other services keep gRPC's defaults.

Tests: 22 unit tests (12 REST over webmock, 10 gRPC against a fake
ManifestService stub answering with real proto messages) cover the base64
round trip, unset sizes as nil, the both-or-neither check, the embed_roots
default and presence, and the 400 / INVALID_ARGUMENT mapping. Two of them
check the gRPC limit: the channel args the manifest stub is built with,
and an in-process server sending a 5 MiB manifest that a default stub
refuses (RESOURCE_EXHAUSTED) and the client receives and decodes. The full
suite passes (151 runs, the two live tests skipped).

test/test_live_manifests.rb is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES (ANTD_TEST_GRPC adds the gRPC run). Against a
10-node devnet it passed over REST and gRPC (118 assertions): a build from
two public addresses and a private DataMap, with paths; decoding the bytes
and the link with resolve, exact sizes matching the fixtures and the
DataMap entry embedded; a bare address as a file link; the fixture
manifest link; and a bad path as BadRequestError. It pays nothing.

examples/09_manifests.rb (ANTD_BASE_URL) stored two pieces of public data,
built a 184-byte manifest with its link, and decoded the link with resolve
(26 and 43 bytes) against the same daemon.

Stubs regenerated with grpc-tools 1.84.0, the version the script pins;
the existing stubs regenerate byte-identically.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
build_manifest, decode_manifest and decode_manifest_link refused bad input
with ArgumentError, so a caller could tell a client-side refusal from the
daemon's 400. They now raise BadRequestError (status 400), with the
daemon's wording, on both Antd::Client and Antd::GrpcClient.

- build_manifest collects every bad entry (both data_map and address, or
  neither; nil and "" count as unset) and reports them as the daemon's
  entry_errors_message 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 text is bare, with no SDK prefix.
- An empty manifest or link raises "needs a manifest (base64 .ant bytes) or
  a link".
- The SDK's other input checks stay (a size that is not a non-negative
  Integer, a Hash with an unknown key, an entry that is neither a Hash nor a
  ManifestEntryInput), now as BadRequestError named "entries[i]: ..." and
  aggregated the same way; entries that is not an Array raises "entries must
  be an Array".
- Response parsing failures (invalid base64 from the daemon) stay
  InternalError.

Tests assert the type, status 400 and exact message, including a two-bad-
entry case and the 20-entry cap, over REST and gRPC. The live test, which
passed over both transports (136 assertions), sends the same bad request
past the check over REST and compares the daemon's message with the
client's once the daemon's "Bad request: " prefix is set aside. The full
suite passes with -w (152 runs, the two live tests skipped).

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:07
@Nic-dorman
Nic-dorman changed the base branch from nic/v2-1570-antd-manifest-endpoints to main October 16, 2026 14:33

@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.

APPROVE: #351 — Ruby REST and gRPC

Reviewed head: 9461db70c83df9bd5ad6a776b950e7d1590613c1. Original base: 6c907eda10948c37cd92dd921d9dd5f68bd3e817.

No material blocker remains after the source-backed adjudication below.

Actual panel coverage: 3/6 scoped opinions — partial panel

Independent first passes used GLM 5.3/OpenRouter, local Qwen 3.8 and local DS4 Flash, two roles per evidence scope. A narrowed retry replaces a missing opinion; it is not an extra vote. This is not full-team consensus.

  • source-1 / correctness: GLM 5.3 APPROVE; Qwen 3.8 APPROVE; DS4 Flash unavailable after same-model retry (Incomplete termination; unavailable seat)
  • source-1 / adversarial: GLM 5.3 unavailable after same-model retry (Incomplete termination; unavailable seat); Qwen 3.8 CHANGES_REQUIRED; DS4 Flash unavailable after same-model retry (Incomplete termination; unavailable seat)

Evidence and dissent adjudication

Qwen adversarial requested changes on the assumption that manifest sizes are JSON strings. The actual Serde DTO and axum handler emit numeric u64 sizes, not proto-JSON strings. The parent compiled that DTO and verified omission/zero/positive/u64-max numbers; the Ruby parser probe preserves them. Ruby Integers do not have the proposed fixed Integer::MAX ceiling. Shared Antd.manifest_entry_inputs runs before the protobuf mapper. Thus the stated defect is not on the supported request path. GLM mentioned possible TLS downgrade; grpc_client.rb:42-46 already offers an insecure target-only channel API, so the new stub does not bypass an existing credentials option. These factual resolutions are parent adjudication, not changed model votes.

Source authority: actual daemon DTO and REST serializer. These are Serde REST fields; a proto uint64 declaration alone does not establish proto-JSON serialization on this endpoint.

Verified checks and limits

Ruby 4.0.7: a direct probe of the actual Client#manifest_size passed nil, zero, 123 and u64 max. Full Ruby REST/gRPC suites remain UNRUN: earlier attempts stopped on missing webmock/minitest and minitest/mock dependencies. No paid/live daemon test.
Head CI: {"SUCCESS": 7}. The live head was rechecked unchanged. The base moved to ea389c79c2ac40601c574e93031a66a9791db1d2: the reviewed diff is byte-identical, no moved-base path overlaps the PR, merge base is unchanged, and git merge-tree succeeds. This proves a clean trial merge, not integrated runtime behaviour.

Scope

Panel production/example fragments: antd-ruby/examples/09_manifests.rb; antd-ruby/lib/antd.rb; antd-ruby/lib/antd/grpc_manifests.rb; antd-ruby/lib/antd/manifests.rb; antd-ruby/scripts/generate-protos.sh.
Not sent as full diff fragments to the panel: antd-ruby/README.md [docs]; antd-ruby/lib/antd/v1/manifests_pb.rb [generated]; antd-ruby/lib/antd/v1/manifests_services_pb.rb [generated]; antd-ruby/test/test_grpc_manifests.rb [tests]; antd-ruby/test/test_live_manifests.rb [tests]; antd-ruby/test/test_manifests.rb [tests]. Separate source-worker review covers the full recorded file inventory; that worker is not a named-model vote. Test execution above is separate from panel inspection. Checked wire base64/raw-byte boundaries, unset versus zero, default root embedding, input validation, endpoint paths, exports and transport limits where applicable. No live payment, deployment, merge or Linear state change.

main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-ruby/README.md antd-ruby/scripts/generate-protos.sh
The result matches #367 for antd-ruby byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman merged commit 2c52489 into main Oct 19, 2026
7 checks passed
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