Skip to content

feat(antd-zig): manifest API (REST) - #352

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

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

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 gains the daemon's ADR-0006 manifest surface, mirroring antd-go:

  • buildManifest(entries, options): POST /v1/manifests/build
  • decodeManifest(bytes, resolve) and decodeManifestLink(link, resolve):
    POST /v1/manifests/decode

The types, request builders and response parsers live in a new
src/manifests.zig; antd.zig only gains the three methods and the type
re-exports (ManifestEntryInput, BuildManifestOptions, ManifestEntryInfo,
BuiltManifest, DecodedManifest). BuildManifestOptions takes embed_roots
as a field defaulting to true, so .{} embeds roots as the daemon's
default does; false is antd-go's KeepShrunkDataMaps. embed_roots is
always sent. Nothing is uploaded or paid for, which the doc comments and
the README say; resolve and root embedding read from the network, so a
call can time out and retrying is safe.

The .ant bytes are raw in the API. They travel as standard base64 in the
JSON (manifest in the build response and the decode request), encoded
and decoded inside the SDK; invalid base64 in a response fails with
error.JsonError.

Sizes are ?u64. An unset size or exact_size reads as null, whether the
daemon left it out or sent null, and never as 0; a size of 0 is sent and
read as 0. A size that is not a JSON integer within u64 (a float, a
negative, a string, or past 2^64-1) fails with error.JsonError instead of
reading as unset. Optional strings (path, data_map and error_message on
an entry, the decoded name and torrent info hashes, the built link) are
null when left out or empty. The wire's error is error_message, since
error is a Zig keyword. Every result has a deinit that frees the bytes,
the strings and the entries.

Errors use the existing set; AntdError gains no members. An entry that
sets both or neither of data_map and address, like antd-go's
checkManifestEntries, and empty decode input are refused before sending
with error.BadRequest, and getLastError() names the entry with
status_code 0. The daemon's 400 (an invalid path such as ../x) is
error.BadRequest with its entries[i]: ... message kept. One entry's
resolve failure is that entry's error_message, not a failed call.

Not breaking: the Client and the existing types are unchanged, and the
new names are additions. REST only, so there is no gRPC receive limit to
raise.

Tests: src/manifest_tests.zig (14 tests) runs the Client against a mock
daemon on a loopback socket, under std.testing.allocator, so every
success and error path is leak-checked. It covers the request bodies,
the base64 round trip of all 256 byte values, unset sizes as null and 0
as 0, sizes up to u64 max, the both-or-neither check before sending, the
embed_roots default, a 400 as BadRequest with the daemon's message, and
malformed responses. src/live_manifests_test.zig is gated on
ANTD_TEST_URL and ANTD_TEST_MANIFEST_ADDRESSES and makes no paid calls.
zig fmt --check and zig build test (Zig 0.14.1) pass: 71 tests, plus the
live one, which passed against the manifest-enabled daemon on a 10-node
devnet. It built a manifest from two public addresses and a private
DataMap with paths, checked kinds, effective names, content addresses
and the link, decoded the bytes and the link with resolve (exact sizes
46, 85 and 82, the DataMap entry embedded with its DataMap), decoded a
bare address as a file link and the fixture link, and got BadRequest
for both sources (before sending) and for ../x (the daemon's 400 naming
entries[1]).

The new examples/10-manifests.zig (optional ANTD_BASE_URL) ran on the
same devnet: it uploaded two notes, built a 184-byte manifest with name
example and paths notes/one.txt and notes/two.txt, printed its link and
entries, and decoded the link with resolve to exact sizes 31 and 48.

Overlap with this SDK's pointer PR #337. A trial merge conflicts only in: antd-zig/README.md, antd-zig/build.zig. 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
Client gains the daemon's ADR-0006 manifest surface, mirroring antd-go:
- buildManifest(entries, options): POST /v1/manifests/build
- decodeManifest(bytes, resolve) and decodeManifestLink(link, resolve):
  POST /v1/manifests/decode

The types, request builders and response parsers live in a new
src/manifests.zig; antd.zig only gains the three methods and the type
re-exports (ManifestEntryInput, BuildManifestOptions, ManifestEntryInfo,
BuiltManifest, DecodedManifest). BuildManifestOptions takes embed_roots
as a field defaulting to true, so `.{}` embeds roots as the daemon's
default does; false is antd-go's KeepShrunkDataMaps. embed_roots is
always sent. Nothing is uploaded or paid for, which the doc comments and
the README say; resolve and root embedding read from the network, so a
call can time out and retrying is safe.

The .ant bytes are raw in the API. They travel as standard base64 in the
JSON (`manifest` in the build response and the decode request), encoded
and decoded inside the SDK; invalid base64 in a response fails with
error.JsonError.

Sizes are ?u64. An unset size or exact_size reads as null, whether the
daemon left it out or sent null, and never as 0; a size of 0 is sent and
read as 0. A size that is not a JSON integer within u64 (a float, a
negative, a string, or past 2^64-1) fails with error.JsonError instead of
reading as unset. Optional strings (path, data_map and error_message on
an entry, the decoded name and torrent info hashes, the built link) are
null when left out or empty. The wire's `error` is error_message, since
`error` is a Zig keyword. Every result has a deinit that frees the bytes,
the strings and the entries.

Errors use the existing set; AntdError gains no members. An entry that
sets both or neither of data_map and address, like antd-go's
checkManifestEntries, and empty decode input are refused before sending
with error.BadRequest, and getLastError() names the entry with
status_code 0. The daemon's 400 (an invalid path such as ../x) is
error.BadRequest with its `entries[i]: ...` message kept. One entry's
resolve failure is that entry's error_message, not a failed call.

Not breaking: the Client and the existing types are unchanged, and the
new names are additions. REST only, so there is no gRPC receive limit to
raise.

Tests: src/manifest_tests.zig (14 tests) runs the Client against a mock
daemon on a loopback socket, under std.testing.allocator, so every
success and error path is leak-checked. It covers the request bodies,
the base64 round trip of all 256 byte values, unset sizes as null and 0
as 0, sizes up to u64 max, the both-or-neither check before sending, the
embed_roots default, a 400 as BadRequest with the daemon's message, and
malformed responses. src/live_manifests_test.zig is gated on
ANTD_TEST_URL and ANTD_TEST_MANIFEST_ADDRESSES and makes no paid calls.
zig fmt --check and zig build test (Zig 0.14.1) pass: 71 tests, plus the
live one, which passed against the manifest-enabled daemon on a 10-node
devnet. It built a manifest from two public addresses and a private
DataMap with paths, checked kinds, effective names, content addresses
and the link, decoded the bytes and the link with resolve (exact sizes
46, 85 and 82, the DataMap entry embedded with its DataMap), decoded a
bare address as a file link and the fixture link, and got BadRequest
for both sources (before sending) and for ../x (the daemon's 400 naming
entries[1]).

The new examples/10-manifests.zig (optional ANTD_BASE_URL) ran on the
same devnet: it uploaded two notes, built a 184-byte manifest with name
example and paths notes/one.txt and notes/two.txt, printed its link and
entries, and decoded the link with resolve to exact sizes 31 and 48.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A manifest input the SDK refuses before sending now reads exactly like
the daemon's 400, so callers cannot tell the two apart. The error was
already error.BadRequest; getLastError() now reports status_code 400
instead of 0.

The both-or-neither source check collects every bad entry, as the
daemon's parse_entries does, and the message is the daemon's own:
`N invalid entries: entries[i]: give data_map or address, not both;
entries[j]: needs a data_map or an address`, with at most 20 entries
named and `; and K more` after that. An empty string counts as unset.
Empty decode input (bytes or link) reports "needs a manifest (base64
.ant bytes) or a link". Response parsing failures are unchanged.

The message is built with the Client's allocator and freed after
setLastError copies it, so the paths stay leak-free under
std.testing.allocator.

Tests assert the status and exact message for one bad entry, two bad
entries, empty strings, 23 bad entries (20 named, "and 3 more") and
both empty-input cases. The live test also sends both sources past the
client check with raw REST and checks the daemon's 400 contains the
client's message. zig fmt --check passes; 72 tests pass without the live
env and 73 with it, against the devnet daemon.

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: #352 — Zig REST

Reviewed head: a3c3dd2f13771219333f561e52d65461058db42e. 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 unavailable after same-model retry (Incomplete termination; unavailable seat); Qwen 3.8 APPROVE; DS4 Flash unavailable after same-model retry (Incomplete termination; unavailable seat)
  • source-1 / adversarial: GLM 5.3 APPROVE; Qwen 3.8 APPROVE; DS4 Flash unavailable after same-model retry (Incomplete termination; unavailable seat)

Evidence and dissent adjudication

No material defect found in the covered production changes. Missing/malformed optional-response fields remain lenient; the server always emits the required fields. Do not treat the compile-only result as a test-suite pass or claim Zig 0.16 compatibility.

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

System Zig 0.16.0 zig build test failed on changed std APIs. Downloaded official Zig 0.14.1 into scratch and checked its archive SHA-256. Zig 0.14.1 test src/tests.zig -fno-emit-bin exited 0 (compile-only). Native zig build test was blocked while linking the build runner on this macOS SDK (undefined libc symbols); test execution is NOT verified. No CI checks are reported; absence is not a pass. No paid/live daemon test.
Head CI: no reported checks (not a pass). 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-zig/build.zig; antd-zig/examples/10-manifests.zig; antd-zig/src/antd.zig; antd-zig/src/manifests.zig.
Not sent as full diff fragments to the panel: antd-zig/README.md [docs]; antd-zig/src/live_manifests_test.zig [tests]; antd-zig/src/manifest_tests.zig [tests]; antd-zig/src/tests.zig [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-zig/README.md antd-zig/build.zig
The result matches #367 for antd-zig byte for byte.

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