Skip to content

feat(antd-kotlin): manifest API (REST + gRPC) - #358

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

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

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.

Both clients, and IAntdClient, can now build and read ADR-0006 .ant
manifests and ant:// links through the daemon:

  • buildManifest(entries, options), from ManifestEntryInput values that
    each set exactly one of a hex dataMap (embedded) or address
    (public), with an optional path and size. BuildManifestOptions carries
    the name, the torrent info hashes and embedRoots.
  • decodeManifest(bytes, resolve) and decodeManifestLink(link, resolve),
    for .ant bytes and for a manifest link, file link or bare address.

They return BuiltManifest (the bytes, the link, or null when it does not
fit 1,500 bytes, and the entries) and DecodedManifest (source, name,
both torrent hashes, the entries), whose ManifestEntryInfo mirrors the
daemon's. Nothing is uploaded or paid for. The types and the wire
mapping live in a new Manifests.kt, so each client gains only three thin
overrides, placed between the file and wallet methods, away from where
the pointer branch adds its own.

The public API takes and returns raw bytes on both transports. REST
carries them as base64 in the JSON manifest field and the client
converts; a response whose manifest is not base64 is an
InternalException. gRPC carries proto bytes. manifests.proto is copied
byte-for-byte into lib/src/main/proto, so the kotlin-proto-copy drift
check passes. Stubs are generated at build time as before (protoc
4.27.2, protoc-gen-grpc-java 1.65.1, protoc-gen-grpc-kotlin 1.4.1),
nothing generated is committed, and the other proto copies are
unchanged.

Sizes are ULong?, as the SDK's other sizes are ULong. Unset is null,
never 0: REST omits the field, and gRPC's proto3 optional fields are
read by presence (hasSize, hasExactSize, hasPath, hasName), not by
their zero value. Sizes go out as exact u64 JSON numbers and as all 64
bits of the proto's uint64. The proto's plain strings that are empty
when unset (link, data_map, error, the torrent hashes) read as null, as
REST's omitted fields do. embedRoots defaults to true and is always
sent, so the wire never relies on the daemon's own default.

The manifest stub raises grpc-java's message limits to 256 MiB both
ways (withMaxInboundMessageSize and withMaxOutboundMessageSize, antd-go's
value), because a manifest can be 64 MiB and the default receive limit
is 4 MiB. Every call on that stub carries them; the other services keep
the defaults.

An entry with both or neither source, or empty decode input, is an
IllegalArgumentException before anything is sent, as antd-go's
checkManifestEntries is a plain error. A daemon rejection keeps the
daemon's message, which names entries[i], and is a BadRequestException
on both transports (400, INVALID_ARGUMENT). One entry's resolve failure
is in its error. There are no new exceptions.

Not breaking: the three methods have default bodies on IAntdClient that
throw UnsupportedOperationException naming the method, as the pointer
methods do, so a caller's own implementation keeps compiling.

Tests: 24 unit tests over a mock REST daemon and an in-process gRPC
server. They cover the base64 round trip, unset sizes reading as null,
sizes up to u64::MAX, the both-or-neither check, the embed_roots
default, presence on every optional field, and the 400 and
INVALID_ARGUMENT mapping. One gRPC test reads the manifest stub's call
options. Another sends and receives a 5 MiB manifest over a loopback
HTTP/2 connection, where a stub with the defaults fails with
RESOURCE_EXHAUSTED. A compat test implements IAntdClient without the
manifest methods.

LiveManifestsTest is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES and makes no paid calls. It passed against
a 10-node devnet, with ANTD_TEST_GRPC set:

  • a build from two public addresses and a private DataMap, with the
    kinds, names, content addresses and link checked
  • decodes of the bytes and the link, with and without resolve: exact
    sizes match the fixtures, and the DataMap entry is embedded with its
    DataMap
  • a decode of the fixture link
  • a BadRequestException naming entries[0] for the path ../x
  • the same over gRPC, which builds the same bytes as REST

./gradlew clean build on dev2: 128 tests, 127 passed and the live test
skipped without its env, no compiler warnings.

Example 09 (Example09Manifests.kt) uploads two public notes, builds a
manifest of them as notes/one.txt and notes/two.txt named "example",
prints the link and each entry's content address, and decodes the link
with resolve, checking the exact sizes. It reads ANTD_BASE_URL and is
left out of "all", since it needs the antd release after 0.15.0. It ran
clean against the same devnet: a 184-byte manifest whose link decoded
with exact sizes 39 and 48.

Overlap with this SDK's pointer PR #340. A trial merge conflicts only in: antd-kotlin/examples/src/main/kotlin/com/autonomi/examples/Main.kt. 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:22
Both clients, and IAntdClient, can now build and read ADR-0006 `.ant`
manifests and `ant://` links through the daemon:
- buildManifest(entries, options), from ManifestEntryInput values that
  each set exactly one of a hex `dataMap` (embedded) or `address`
  (public), with an optional path and size. BuildManifestOptions carries
  the name, the torrent info hashes and embedRoots.
- decodeManifest(bytes, resolve) and decodeManifestLink(link, resolve),
  for `.ant` bytes and for a manifest link, file link or bare address.

They return BuiltManifest (the bytes, the link, or null when it does not
fit 1,500 bytes, and the entries) and DecodedManifest (source, name,
both torrent hashes, the entries), whose ManifestEntryInfo mirrors the
daemon's. Nothing is uploaded or paid for. The types and the wire
mapping live in a new Manifests.kt, so each client gains only three thin
overrides, placed between the file and wallet methods, away from where
the pointer branch adds its own.

The public API takes and returns raw bytes on both transports. REST
carries them as base64 in the JSON `manifest` field and the client
converts; a response whose manifest is not base64 is an
InternalException. gRPC carries proto `bytes`. manifests.proto is copied
byte-for-byte into lib/src/main/proto, so the kotlin-proto-copy drift
check passes. Stubs are generated at build time as before (protoc
4.27.2, protoc-gen-grpc-java 1.65.1, protoc-gen-grpc-kotlin 1.4.1),
nothing generated is committed, and the other proto copies are
unchanged.

Sizes are ULong?, as the SDK's other sizes are ULong. Unset is null,
never 0: REST omits the field, and gRPC's proto3 `optional` fields are
read by presence (hasSize, hasExactSize, hasPath, hasName), not by
their zero value. Sizes go out as exact u64 JSON numbers and as all 64
bits of the proto's uint64. The proto's plain strings that are empty
when unset (link, data_map, error, the torrent hashes) read as null, as
REST's omitted fields do. embedRoots defaults to true and is always
sent, so the wire never relies on the daemon's own default.

The manifest stub raises grpc-java's message limits to 256 MiB both
ways (withMaxInboundMessageSize and withMaxOutboundMessageSize, antd-go's
value), because a manifest can be 64 MiB and the default receive limit
is 4 MiB. Every call on that stub carries them; the other services keep
the defaults.

An entry with both or neither source, or empty decode input, is an
IllegalArgumentException before anything is sent, as antd-go's
checkManifestEntries is a plain error. A daemon rejection keeps the
daemon's message, which names `entries[i]`, and is a BadRequestException
on both transports (400, INVALID_ARGUMENT). One entry's resolve failure
is in its `error`. There are no new exceptions.

Not breaking: the three methods have default bodies on IAntdClient that
throw UnsupportedOperationException naming the method, as the pointer
methods do, so a caller's own implementation keeps compiling.

Tests: 24 unit tests over a mock REST daemon and an in-process gRPC
server. They cover the base64 round trip, unset sizes reading as null,
sizes up to u64::MAX, the both-or-neither check, the embed_roots
default, presence on every optional field, and the 400 and
INVALID_ARGUMENT mapping. One gRPC test reads the manifest stub's call
options. Another sends and receives a 5 MiB manifest over a loopback
HTTP/2 connection, where a stub with the defaults fails with
RESOURCE_EXHAUSTED. A compat test implements IAntdClient without the
manifest methods.

LiveManifestsTest is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES and makes no paid calls. It passed against
a 10-node devnet, with ANTD_TEST_GRPC set:
- a build from two public addresses and a private DataMap, with the
  kinds, names, content addresses and link checked
- decodes of the bytes and the link, with and without resolve: exact
  sizes match the fixtures, and the DataMap entry is embedded with its
  DataMap
- a decode of the fixture link
- a BadRequestException naming entries[0] for the path `../x`
- the same over gRPC, which builds the same bytes as REST

./gradlew clean build on dev2: 128 tests, 127 passed and the live test
skipped without its env, no compiler warnings.

Example 09 (Example09Manifests.kt) uploads two public notes, builds a
manifest of them as notes/one.txt and notes/two.txt named "example",
prints the link and each entry's content address, and decodes the link
with resolve, checking the exact sizes. It reads ANTD_BASE_URL and is
left out of "all", since it needs the antd release after 0.15.0. It ran
clean against the same devnet: a 184-byte manifest whose link decoded
with exact sizes 39 and 48.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The both-or-neither source check and the empty decode input threw
IllegalArgumentException with an SDK-flavoured message. They now throw
BadRequestException with status 400, the error a daemon 400 maps to, so
a caller cannot tell a client-side refusal from the daemon's.

buildManifest collects every bad entry and reports them together in the
daemon's wording from parse_entries and entry_errors_message:
"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 string counts as unset. An empty manifest or
link is "needs a manifest (base64 .ant bytes) or a link". The text is
bare, as gRPC sends it; the REST error field adds a "Bad request: "
prefix that the client does not synthesize. Response parsing failures
(bad base64) stay InternalException.

Tests now assert type, status and exact message on both transports,
including two bad entries and the 20-entry cap. The live test sends the
same bad request to the daemon and checks its 400 body contains the
client's message.

./gradlew clean build on dev2: 129 tests, 128 passed and the live test
skipped without its env, no compiler warnings. LiveManifestsTest passed
against the devnet, REST and gRPC.

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 commented Oct 17, 2026

Copy link
Copy Markdown
Member

PROVISIONAL: #358 — Kotlin manifest API

Repository: WithAutonomi/ant-sdk. Reviewed head: 602ce320756575c9590302e033eec8336c1d517d. Original merge base: 6c907eda10948c37cd92dd921d9dd5f68bd3e817.

No material code defect found in the source pass or the completed Qwen reviews. No formal GitHub verdict: the panel is below the two-model quorum. This is a review-coverage limit, not a reason to request code changes. A human maintainer can submit the formal review.

Actual review-team participation: 2/6 scoped seats; 1/3 named models

Two roles were requested from GLM 5.3/OpenRouter (z-ai/glm-5.3), local Qwen 3.8 (qwen38-27b) and local DS4 Flash (deepseek-v4-flash). Missing seats received one narrowed retry on the same model. No replacement, synthesis or source worker is counted as an extra vote. This is not team consensus.

  • source-1 / correctness: glm: unavailable after same-model retry (Exceeded the request deadline before the full response body); qwen: APPROVE; ds4: unavailable after same-model retry (Incomplete termination; unavailable seat).
  • source-1 / adversarial: glm: unavailable after same-model retry (Incomplete termination; unavailable seat); qwen: APPROVE; ds4: unavailable after same-model retry (Incomplete termination; unavailable seat).

Source findings

  • Manifests.kt:158–179 and both client adapters validate invalid inputs before sending. The entry-error wording and cap match the daemon. These are local input refusals, not payment or network retries.
  • Manifests.kt:188–218,262–268,286–324: REST uses base64 inside JSON; gRPC uses raw bytes. REST sizes use nullable ULong and JSON numbers, matching the daemon Serde DTO, not an assumed proto-JSON encoding. gRPC optional fields use presence checks, and bit-preserving signed/unsigned conversion retains the full uint64 range. Zero remains distinct from absence.
  • Manifests.kt:201,291: both transports explicitly send embed_roots, including the default true. AntdGrpcClient.kt:393–395 raises both manifest-stub message limits to 256 MiB without changing other service stubs. Validation precedes request creation in AntdRestClient.kt:438–452 and AntdGrpcClient.kt:397–418.
  • The checked-in proto copy matches the daemon. The interface supplies default method bodies for existing implementers; both concrete clients supply the manifest methods. The README and Example 09 distinguish the paid data uploads from the free manifest operations; Example 09 is deliberately excluded from all.

Tests and evidence limits

  • Source inspected: the new manifest mapping module, client adapters, interface, proto, tests, README and example. The panel received its scoped production/example evidence; the separate source pass covers the full recorded changed-file inventory. Source verification is not an additional independent model vote.
  • The REST and gRPC test source covers unset versus zero, uint64 maximum, byte conversion, refusal-before-send, and message limits. ManifestGrpcTest.kt:299–339 contains a real loopback HTTP/2 large-message test with a default-limit control, but it was not run here.
  • No Kotlin build or tests executed here. java -version and /usr/libexec/java_home -V both failed: no Java runtime is installed. No Gradle/JDK/dependencies were installed. The author-reported Gradle/devnet results are not independently verified and are not counted as passing.
  • Live head rechecked unchanged. GitHub check snapshot: {"SUCCESS": 7} — these are proto-drift checks, not Kotlin compilation/test evidence. Live base ea389c79c2ac40601c574e93031a66a9791db1d2: reviewed diff byte-identical, merge base unchanged, no overlapping base-drift paths, trial merge exit 0. A clean trial merge is not runtime integration proof.
  • No live daemon, payment, testnet, merge, deployment, source change or Linear state change.

@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 verdict: APPROVE — partial panel

Repository: WithAutonomi/ant-sdk
Head: 602ce320756575c9590302e033eec8336c1d517d
Current target base verified at publication: ea389c79c2ac40601c574e93031a66a9791db1d2

Coverage: 4/6 role seats reviewed; 2/3 named models. GLM 5.3 (z-ai/glm-5.3, OpenRouter) and local Qwen 3.8 (qwen38-27b) each completed correctness and adversarial reviews of the same source-1 evidence hash 756390036517dc7e63ff89c6b07e1a582c73b04ceafb748f25f189fbaf77127a. Qwen's completed exact-head receipts were preserved; GLM's previously missing seats were rerun. Missing: local DS4 Flash (deepseek-v4-flash) correctness and adversarial; earlier attempts timed out or ended without a complete verdict. A fresh bounded DS4 recovery is queued separately and is NOT counted here. This is a quorum partial verdict, not full-team consensus.

Source and findings

Reviewed Kotlin REST/gRPC manifest mapping, input checks, transport interface compatibility and the example. No material defect remains in the supplied production/schema scope. The daemon uses serde Option with no string serializer for size/exact_size; REST returns the struct directly as Json, so the JSON-number assumption is supported.

Non-blocking: empty-entry handling is delegated to the daemon; the client surfaces its error. Test files exist but were excluded from the production bundle; a reviewer’s statement that no tests appeared in the bundle is not evidence that the PR has no tests.

Checks and limits

Local Kotlin tests were NOT run: this host has no JDK. The live GitHub checks, including the proto drift check, must be green at publication. No live-daemon or paid example was run. These are coverage limits, not passing runtime tests.

The head is unchanged from the frozen evidence. Saved base-drift revalidation found the reviewed diff unchanged, no overlapping base edits, and a clean trial merge; the publisher re-checks the live head, base and CI immediately before this review.

No unresolved material dissent among the completed reviews. This verdict does not authorise merge, release, deployment, or payment. No merge performed.

main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-kotlin/examples/src/main/kotlin/com/autonomi/examples/Main.kt
Also carries the #367 fix-up for this SDK: antd-kotlin/README.md
The result matches #367 for antd-kotlin byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Nic-dorman
Nic-dorman merged commit e72691d 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