Skip to content

DO NOT MERGE: CI preview of the pointer + manifest fan-outs landed together - #367

Merged
Nic-dorman merged 111 commits into
mainfrom
nic/stack-integration-preview
Oct 19, 2026
Merged

Nic-dorman merged 111 commits into
mainfrom
nic/stack-integration-preview

Conversation

@Nic-dorman

Copy link
Copy Markdown
Member

DO NOT MERGE. CI preview only; it will be closed once the stack lands.

This branch is #346 (manifest endpoints, which includes #325 and #326) with all 35 fan-out PRs merged on top: the pointer fan-out #327–#343 and the manifest fan-out #348–#365 (not #347). It exists so CI runs over the combined tree before the real PRs land.

What it adds beyond the PRs themselves:

  • Conflict resolutions. 13 manifest PRs conflict with the pointer PRs in the same files: READMEs, the per-SDK proto-generator lists (ruby, elixir, dart, py), the example registries (zig build.zig, kotlin Main.kt, rust Cargo.toml, ant-dev), antd-py/_grpc.py imports, the MCP CI tool count, and the docs (README.md, llms*.txt, skill.md, docs/architecture.md). All of them keep both sides.
  • Land-time fix-ups (e025a9f): the MCP tool count is 31 everywhere; plus C#, Kotlin, Swift and JS example and error text.
  • Dart stubs regenerated (8733d0e) with both protos, so the $N import aliases are right.

When the real PRs land, these resolutions are reused, and the final tree is checked against this one.

🤖 Generated with Claude Code

Nic-dorman and others added 30 commits October 15, 2026 11:56
antd-go/examples/08-pointers and antd-rust/examples/09-pointers.rs
walk the pointer API end to end: store two small pieces of public data,
create a pointer at the first, update it to the second, read it back
with get and resolve, fetch the bytes it leads to, and print the close
group's finality.

The owner comes from ANTD_POINTER_OWNER, the address of a pointer the
daemon holds a key for and that has never been written. The example
creates that pointer, so an address can be used once, and it stops
before paying for anything when the variable is unset. Like the sibling
examples, both talk to the daemon at the default base URL. Each prints a
note that the create and the update pay, and neither retries a write.
The Rust example is registered in Cargo.toml, and both READMEs list
the new example.

Both ran once against antd on a 10-node devnet, each with its own fresh
owner: create at counter 0, update to counter 1, get and resolve to the
second target, its bytes fetched back, finality open with 7/7 peers
answering. go vet, gofmt, golangci-lint, cargo fmt --check and
cargo clippy --all-targets -D warnings are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AntdClient gains the daemon's pointer surface, each with an Async
variant like the rest of the client:
- pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
- pointerGet, pointerResolve, pointerFinality, pointerController
- pointerPrepare (taking a PointerPrepareRequest built with ::create,
  ::update or ::transfer) and pointerFinalize, for an external signer

The models are readonly classes: PointerWriteResult, PointerRecord
(isFinal, since final is a reserved word), PointerTarget,
PointerFinality, PointerFinalState, PointerController,
PointerTransferResult, PointerPrepareResult and PointerFinalizeResult,
plus the PointerTargetKind and PointerOp enums. PointerFinalityStatus
is an enum with an Unknown case for statuses a newer daemon may add;
PointerFinality keeps the wire string in rawStatus. REST's null and an
empty string both read as null for the optional fields.

Counters are canonical decimal strings, not ints. A final state sits
at 2^64-1, past PHP's signed int, and GMP and BCMath are not runtime
dependencies, so PointerCounter::parse() accepts only a string of
digits within the u64 range and throws UnexpectedValueException for
anything else (a JSON number, a sign, a value past u64), never a zero.
PointerCounter::FINAL is the final counter.

Errors map by the body's code where it diverges from the status.
POINTER_FINAL and CONFLICT are new PointerFinalError and ConflictError,
POINTER_FORKED is the existing ForkError, and none of the three folds
into AlreadyExistsError any more. CLOSE_GROUP_SHORTFALL is a new
CloseGroupShortfallError whose retryable is true only for the JSON
boolean true. All three new classes extend AntdError directly. Nothing
retries a paid write; the docblocks and the README say each call pays.
This is additive: no released daemon sends these codes, and PHP has no
exhaustive match over exception classes.

Tests: 60 unit tests over a Guzzle MockHandler cover every method,
including the request bodies, counters up to 2^64-1, malformed
counters, finality's open, forked and unknown shapes, the code mapping
and a single request per failed paid write. tests/LivePointersTest.php
is a round trip against a live daemon on a devnet, gated on
ANTD_TEST_URL and ANTD_TEST_POINTER_OWNERS. It passed against a
10-node devnet:
- an unknown owner refused, then create, update, get and resolve, with
  bytes fetched back through the pointer
- finality, and an async create and get
- a transfer, then POINTER_FINAL on update and transfer
- a prepare up to the quote, with CONFLICT while it is pending and a
  finalize without hashes refused

examples/08-pointers.php (ANTD_POINTER_OWNER, optional ANTD_URL) ran
against the same daemon: create, update, resolve back to the second
target's bytes, finality open with 7/7 peers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RestClient gains the daemon's pointer surface:
- pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
- pointerGet, pointerResolve, pointerFinality, pointerController
- pointerPrepare(owner, op, { includeSignedQuotes }) and pointerFinalize,
  for an external signer

The prepare op is a discriminated union ({ op: "create" | "update",
target, kind? } or { op: "transfer", recipient }), and PointerKind
follows the PaymentMode const-object pattern. The SDK is REST-only, so
there is no gRPC side.

Counters are bigint. The daemon sends them as decimal strings because a
final state sits at 2^64-1 (exported as POINTER_FINAL_COUNTER), past
what a JS number holds exactly. Only a digits-only string up to 2^64-1
is accepted; anything else throws InternalError instead of reading as
zero. Unset values (transferredTo, the finality counter, state,
majority, a transfer's finality) are null. Finality status is a string
union of open, settling, unconfirmed, final and forked, plus "unknown"
for a status a newer daemon adds, with the daemon's string kept in
rawStatus. The record's flag is isFinal, read from the REST is_final.

Errors map by the body's code where it diverges from the status.
POINTER_FINAL (PointerFinalError), POINTER_FORKED (the existing
ForkError) and CONFLICT (ConflictError) are 409s and no longer read as
AlreadyExistsError; create's ALREADY_EXISTS still does.
CLOSE_GROUP_SHORTFALL is a 503 that becomes CloseGroupShortfallError,
with retryable set only when the body's retryable is true (a pointer
finalize that kept the paid state). It extends AntdError, not
ServiceUnavailableError. The client never retries a request, and the
docs say not to retry a paid pointer write.

Tests: 60 unit tests over a mocked fetch cover every method's request
and parsing, counters up to 2^64-1 with malformed ones rejected, an
unknown finality status, and the error-code mapping. The suite is now
168 passing, with typecheck and build clean. src/live-pointers.test.ts
is a round trip gated on ANTD_TEST_URL and ANTD_TEST_POINTER_OWNERS. It
passed against a 10-node devnet:
- create, AlreadyExists on a second create, get, resolve, with bytes
  fetched back through the pointer
- update, and finality open at counter 1
- a transfer reported final, the record at 2^64-1, the controller, and
  POINTER_FINAL on update and transfer
- a prepare up to the quote, CONFLICT while it is pending, and a
  finalize without payment refused
examples/08-pointers.ts (ANTD_POINTER_OWNER, optional ANTD_BASE_URL)
ran clean against the same daemon: create at counter 0, update at
counter 1, resolve to the second target's bytes, finality open with
7 of 7 answered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The REST client gains the daemon's pointer surface:
- pointer_keys, pointer_create, pointer_update, pointer_transfer
- pointer_get, pointer_resolve, pointer_finality, pointer_controller
- pointer_prepare (with an optional opts table for include_signed_quotes)
  and pointer_finalize, for an external signer

Op tables for prepare come from pointer_create_op, pointer_update_op and
pointer_transfer_op. PointerKind, PointerOp and PointerFinalityStatus
hold the wire strings. A finality status this SDK does not know reads
as "unknown", with the wire value kept in raw_status. REST nulls
(transferred_to, the finality counter, state, majority) read as nil.

Counters stay decimal strings. A final state sits at 2^64-1, and no
Lua number holds that: LuaJIT and 5.1 numbers are doubles, exact to
2^53, and 5.4 integers are signed. Every counter read is checked
(ASCII digits only, at most 2^64-1) and loses its leading zeros, so
equal counters are equal strings. Anything else is an internal error,
never a 0. compare_pointer_counters compares two by value, and
POINTER_FINAL_COUNTER names the final one. normalize_address strips a
0x prefix for comparisons. Address path segments are percent-encoded,
so an address cannot reach another route.

Errors map by the body's code where it diverges from the status.
POINTER_FINAL, POINTER_FORKED and CONFLICT are 409s with their own
types (pointer_final, pointer_forked, conflict) and no longer read as
already_exists. ALREADY_EXISTS keeps its mapping.
CLOSE_GROUP_SHORTFALL is a 503 typed close_group_shortfall, where it
used to read as service_unavailable. Its retryable flag is true only
for a JSON true, which marks a pointer finalize that kept the paid
state. The README's upgrade note covers the 503 change, since a
pointer-capable daemon reports a shortfall that way on any call. The
SDK never retries a paid write, and the README says each one pays.

Tests: spec/pointers_spec.lua has 48 busted cases over mocked HTTP.
They cover every method's request and parsing, counters up to 2^64-1
and the malformed ones, the error mapping, and a check that each paid
write is sent exactly once. The full suite passes under Lua 5.1 and
LuaJIT (183 successes, 1 pending). spec/live_pointers_spec.lua is
pending unless ANTD_TEST_URL and ANTD_TEST_POINTER_OWNERS are set.
Against a 10-node devnet it passed:
- create, update, get, resolve, with bytes fetched back through the
  pointer
- a 0x-prefixed read, and finality
- a transfer, the final counter and the controller, then
  pointer_final on update and transfer
- a prepare up to the quote, with conflict while it is pending and a
  refused finalize without the payment

examples/08-pointers.lua uploads two pieces of public data, creates a
pointer at the first, updates it to the second, reads it back and
prints its finality (open, 7 of 7 answered). It ran against the same
devnet under LuaJIT and Lua 5.4. It reads ANTD_POINTER_OWNER, and
ANTD_BASE_URL to override the daemon URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Antd::Client and Antd::GrpcClient gain the daemon's pointer surface:
- pointer_keys, pointer_create, pointer_update, pointer_transfer
- pointer_get, pointer_resolve, pointer_finality, pointer_controller
- pointer_prepare (with include_signed_quotes:) and pointer_finalize, for
  an external signer

The models are keyword structs: PointerWriteResult, PointerRecord,
PointerTarget, PointerFinality, PointerFinalState, PointerController,
PointerTransferResult, PointerPrepareResult, PointerFinalizeResult and
SignedQuoteEntry, plus PointerKind, PointerOp and PointerFinalityStatus
string constants. The gRPC client talks to PointerService;
scripts/generate-protos.sh now lists pointers.

Counters are Integers. The daemon sends them as decimal strings, because
a final state sits at u64::MAX (Antd::POINTER_FINAL_COUNTER). Ruby
integers are unbounded, so a counter must be ASCII digits within u64;
anything else raises InternalError and never reads as 0 (Integer() alone
would take a sign, underscores or a radix prefix). Finality status stays
the wire string: a status a newer daemon adds is kept verbatim, and
status_known? says so. REST's nulls and gRPC's empty strings and absent
messages both read as nil.

Errors map by the daemon's code where it shares a status with older
errors. PointerFinalError, PointerForkedError (a ForkError) and
ConflictError are 409s and no longer read as AlreadyExistsError.
CloseGroupShortfallError is a 503 whose retryable says a pointer
finalize kept the paid state; it is not a ServiceUnavailableError. Over
gRPC, grpc_call reads the x-antd-error-code trailer
(GRPC::BadStatus#metadata) on every call, but only trusts it when it
agrees with the status code, so an untagged FAILED_PRECONDITION is still
a PaymentError. retryable then comes from the closing "(paid attempt
retained" hint. The SDK never retries a request, and the README and
method docs say each pointer write pays.

Tests: 59 unit tests over a mocked REST daemon (webmock) and a fake
PointerService stub answering with real proto messages. They cover every
method, the error codes on both transports, and counters up to u64::MAX.
test/test_live_pointers.rb is a round trip against a live daemon, gated
on ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS. It passed
against a 10-node devnet (43 assertions):
- create, update, get, resolve, with bytes fetched back through the
  pointer
- finality over both transports, and a gRPC create and get
- a transfer, then POINTER_FINAL over both transports
- a prepare with signed quotes, with CONFLICT while it is pending

examples/08_pointers.rb (ANTD_POINTER_OWNER, ANTD_BASE_URL) created,
updated, resolved and checked a pointer 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>
Client gains the daemon's pointer surface:
- pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
- pointerGet, pointerResolve, pointerFinality, pointerController
- pointerPrepare (+ pointerPrepareWithOptions for include_signed_quotes)
  and pointerFinalize, for an external signer

The models follow antd-rust. PointerOp is a tagged union of create,
update and transfer. PointerFinalityStatus is a tagged union with an
unknown arm that keeps a status a newer daemon may add. Every result
type has a deinit that frees its nested strings, states and quotes.
Unset values (transferred_to, the finality counter, state and majority)
are null whether the daemon sent null or an empty string. Addresses come
back as bare hex, so PointerKeys.contains and pointerAddressEql ignore a
0x prefix and hex case.

Counters are u64. The daemon sends them as decimal strings, because a
final state's 2^64-1 is past what JSON numbers keep exactly in several
languages. std.fmt.parseInt alone would take a sign and underscores, so
a counter must be ASCII digits within u64; anything else fails the call
with error.JsonError rather than reading as 0.

Errors map by the body's code. POINTER_FINAL, POINTER_FORKED and
CONFLICT are 409s and no longer read as AlreadyExists.
CLOSE_GROUP_SHORTFALL is a 503 that sets ErrorInfo.retryable only for
the JSON true a pointer finalize sends when it kept the paid state.
These are four new AntdError members (PointerFinal, PointerForked,
Conflict, CloseGroupShortfall), so a switch without an else prong needs
new prongs; the README's upgrade note says so. Any call can now return
CloseGroupShortfall where a daemon with the pointer API used to send a
500 (error.Internal). The SDK never retries a pointer write; the doc
comments and README say that each attempt pays.

Tests: src/pointer_tests.zig runs the Client against a mock daemon on a
loopback socket for every method. It checks the request paths and bodies,
the error mapping by code, counters up to u64 max, malformed counters,
and that a failed paid write is sent once. It runs under
std.testing.allocator, so the error paths are checked for leaks too.
src/live_pointers_test.zig is a round trip gated on ANTD_TEST_URL and
ANTD_TEST_POINTER_OWNERS. It passed against a 10-node devnet:
- create, AlreadyExists, get, resolve with the bytes fetched back,
  update, and finality open at counter 1
- a transfer with finality final, then get, controller and resolve
  through the recipient
- POINTER_FINAL on update and transfer
- a prepare with signed quotes, then CONFLICT on update and prepare
  while it is pending, and a finalize without payment refused as
  BadRequest

The new examples/09-pointers.zig (ANTD_POINTER_OWNER, optional
ANTD_BASE_URL) created, updated, resolved and read the finality of a
pointer on the same devnet, and a second run took its AlreadyExists path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Client, AsyncClient and GrpcClient gain the daemon's pointer surface:
- pointer_keys, pointer_create, pointer_update, pointer_transfer
- pointer_get, pointer_resolve, pointer_finality, pointer_controller
- pointer_prepare and pointer_finalize, for an external signer

pointer_prepare takes a PointerPrepareRequest, built with create(),
update() or transfer(); its include_signed_quotes flag is the options
variant and fills signed_quotes with base64 blobs on both transports.
GrpcClient talks to PointerService, whose stubs the antd_grpc build
generates from antd/proto, so there are no committed stubs to drift.

Counters are std::uint64_t (kPointerFinalCounter is 2^64-1), parsed
from the daemon's decimal strings. Anything but plain digits that fit
in 64 bits throws InternalError, never a zero; std::stoull is avoided
because it takes leading whitespace and wraps "-1". Unset values (REST
null, gRPC "" or an absent message) are std::optional. Finality status
is an enum with an Unknown arm, and status_raw keeps the daemon's
string for statuses a newer daemon may add.

New error types for the codes that share a status with older errors:
PointerFinalError, PointerForkedError (a ForkError), ConflictError and
CloseGroupShortfallError, with retryable for a pointer finalize that
kept the paid state. REST reads them from the body's code, so the 409s
no longer read as AlreadyExistsError. The gRPC pointer methods read
the x-antd-error-code trailer and trust it only when it agrees with
the status code, so FAILED_PRECONDITION without it is still a
PaymentError; retryable comes from the message's closing "(paid
attempt retained" hint. Nothing retries a paid write.

Tests: a stub REST daemon covers every method on Client and
AsyncClient, the error mapping, and counters up to 2^64-1 alongside
malformed ones. A mock grpc++ PointerService covers the same over
gRPC, trailers included. The logic-only gRPC tests run the real tag
mapping. tests/live_pointers.cpp is gated on ANTD_TEST_URL,
ANTD_TEST_POINTER_OWNERS and, in a gRPC build, ANTD_TEST_GRPC. It
passed against a 10-node devnet:
- create, update, get and resolve, with bytes fetched back through
  the pointer
- finality, and a gRPC create and get
- a transfer, then POINTER_FINAL over both transports
- a prepare up to the quote, with CONFLICT over both transports while
  it is pending

examples/08-pointers.cpp (ANTD_POINTER_OWNER, optional ANTD_BASE_URL)
ran against the same daemon. Built with g++ 13, CMake 3.28, and for
gRPC the apt protoc 3.21.12 with grpc++ 1.51.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AntdRestClient and AntdGrpcClient gain the daemon's pointer surface:
- PointerKeysAsync, PointerCreateAsync, PointerUpdateAsync,
  PointerTransferAsync
- PointerGetAsync, PointerResolveAsync, PointerFinalityAsync,
  PointerControllerAsync
- PointerPrepareAsync (with PointerPrepareRequest.Create, Update and
  Transfer) and PointerFinalizeAsync, for an external signer

The gRPC client talks to PointerService. Grpc.Tools compiles the protos
at build time, so pointers.proto is added to the csproj's Protobuf list
and no generated code is committed. Both clients are now partial
classes, with the pointer code in their own files.

The methods are also on IAntdClient, with default implementations that
throw NotSupportedException, so a class implementing the interface
outside the package keeps compiling.

Counters are ulong. The daemon sends them as decimal strings, because a
final state's u64::MAX is past what JSON numbers keep exactly in several
languages. They are parsed with NumberStyles.None, so a sign, whitespace
or a non-ASCII digit is an error, never a zero. Finality status is an
enum with an Unknown member for statuses a newer daemon may add, and
RawStatus keeps the wire value. Unset values (null over REST, "" or an
absent message over gRPC) read as null on both transports. Nothing
retries a paid write; the README says each one pays.

Errors map by the daemon's code where it diverges from the status.
PointerFinalException, PointerForkedException and ConflictException
extend ForkException, which every REST 409 has mapped to, so existing
catch blocks still match; a 409 ALREADY_EXISTS stays a plain
ForkException. CloseGroupShortfallException extends AntdException, and
its Retryable flag says a pointer finalize kept the paid state. Over
gRPC the same types come from the x-antd-error-code trailer on
RpcException.Trailers, but only when it agrees with the status code, so
FAILED_PRECONDITION without it is still a PaymentException. The trailer
is read for every call, since a close-group shortfall can come from any
of them. These are new subclasses, so no catch stops compiling, but a
shortfall no longer matches ServiceUnavailableException (REST) or
NetworkException (gRPC); the README's upgrade notes say so.

Tests: 108 new unit tests, 274 in all. They cover every method over a
mock REST daemon and mocked PointerService stubs, the error mapping
both ways including tags on the wrong status, counters up to u64::MAX
and the malformed ones, and the interface's default members.
PointerLiveTests is a round trip against a live daemon on a devnet,
gated on ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS. It
passed against a 10-node devnet:
- create, update, get, resolve, with bytes fetched back through the
  pointer
- finality, and a gRPC create, get and finality
- a transfer, then POINTER_FINAL over both transports
- a prepare up to the quote, with CONFLICT over both transports while
  it is pending

Example 8 (dotnet run -- 8, or -- pointers) uploads two pieces of
public data, points a pointer at the first, updates it to the second,
resolves it and prints its finality. It takes the owner from
ANTD_POINTER_OWNER and the daemon from ANTD_BASE_URL. It is left out of
"all" because every write pays. It ran clean against the same devnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AntdClient, AsyncAntdClient and GrpcAntdClient gain the daemon's
pointer surface, with the same models and exceptions on all three:
- pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
- pointerGet, pointerResolve, pointerFinality, pointerController
- pointerPrepare (with a PointerPrepareOptions overload for signed
  quotes) and pointerFinalize, for an external signer

The models are records: PointerRecord (isFinal()), PointerTarget,
PointerFinality, PointerFinalState, PointerController,
PointerTransferResult, PointerWriteResult, PointerPrepareResult,
PointerFinalizeResult and SignedQuoteEntry. PointerOp is a sealed
interface of Create, Update and Transfer. The finality status is an
enum with an UNKNOWN value for statuses a newer daemon may add, and
rawStatus() keeps the daemon's string. Unset values are null on both
transports: REST's null and gRPC's empty string or absent message.
The gRPC stubs come from the existing protobuf Gradle build, which
already compiles every proto under antd/proto, so nothing generated
is committed.

Counters are BigInteger. The SDK's other 64-bit fields are signed
longs holding values far below 2^63, but a final pointer state sits
at 2^64-1 (PointerRecord.FINAL_COUNTER), which a long can only carry
as -1. A long read with Long.parseUnsignedLong would print and compare
wrong unless every caller used the unsigned helpers, while BigInteger
holds the value exactly and compares correctly. Parsing is strict:
ASCII digits only, at most 2^64-1. Anything else, including a sign, a
non-ASCII digit or a JSON number, is an AntdException, not a zero.

Errors map by the daemon's code where it diverges from the status.
POINTER_FINAL and CONFLICT become the new PointerFinalException and
ConflictException, and POINTER_FORKED becomes the existing
ForkException. None of the three reads as AlreadyExistsException.
CLOSE_GROUP_SHORTFALL becomes CloseGroupShortfallException, whose
isRetryable() says a pointer finalize kept the paid state. The three
new classes extend AntdException directly. The shortfall exception
is not a ServiceUnavailableException or NetworkException, so it reads
the same on both transports. REST reads the body's code. gRPC reads
the x-antd-error-code trailer, but only when it agrees with the status
code, so an untagged FAILED_PRECONDITION is still a PaymentException.
Over gRPC, retryable comes from the message's closing "(paid attempt
retained...)" hint. The trailer mapping applies to every RPC, since
the daemon reports a shortfall on any call. No exception type is
sealed, so the new subclasses do not break callers. A daemon before
the pointer API sent a shortfall as a 500, which read as an
InternalException.

No pointer write is retried. Each call to create, update, transfer or
finalize pays, and the javadoc and README say so.

Tests: 108 new unit tests. PointerRestTest covers both REST clients
against MockWebServer, GrpcPointerTest covers an in-process
PointerService, and Example08PointersTest drives the example against
a mock daemon. Together they cover every method's request and
response, counters from 0 to 2^64-1, rejected counters, null and
empty unset values, and the error mapping both ways, including tags
that disagree with the status. LivePointersTest is a round trip
gated on ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS.
It passed against a 10-node devnet:
- create, update, get, resolve, with bytes fetched back through the
  pointer, and an async read
- REST finality, and a gRPC create and get
- a transfer to a final state at 2^64-1, then the controller
- POINTER_FINAL over both transports, including a gRPC prepare
- a prepare with signed quotes, then CONFLICT over both transports
  and a BAD_REQUEST finalize without payment

Example08Pointers uploads two pieces of public data, creates a
pointer at the first, updates it to the second, resolves and reads it
back, and prints its finality. It takes ANTD_POINTER_OWNER, plus an
optional ANTD_BASE_URL. Against the devnet it printed counter 1, the
second target's bytes, and finality "open" with 7/7 peers answering.

The README gains a pointer section and the new error rows, and no
longer says the SDK has immutable data types only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RestClient, AsyncRestClient, GrpcClient and AsyncGrpcClient gain the
daemon's pointer surface with the same signatures, models and errors:
- pointer_keys, pointer_create, pointer_update, pointer_transfer
- pointer_get, pointer_resolve, pointer_finality, pointer_controller
- pointer_prepare (keyword options, include_signed_quotes among them)
  and pointer_finalize, for an external signer

The models are frozen dataclasses: PointerWriteResult, PointerRecord,
PointerTarget, PointerFinality, PointerFinalState, PointerController,
PointerTransferResult, PointerPrepareResult, PointerFinalizeResult and
SignedQuoteEntry, with the PointerKind, PointerOp and
PointerFinalityStatus enums. Finality status has an UNKNOWN member for
statuses a newer daemon may add, with the daemon's string kept in
raw_status. REST nulls and gRPC empty strings or absent messages both
read as None. The shared mapping lives in a new _pointers module, so a
REST-only install still never imports grpc.

Counters are Python ints. The daemon sends them as decimal strings, since
a final state sits at u64::MAX (POINTER_FINAL_COUNTER). They are parsed
strictly: ASCII digits only, no larger than u64::MAX. int() alone would
take "+1", " 1" and "1_000", so anything that is not a decimal u64 raises
InternalError rather than reading as zero.

Errors map by the daemon's code where it shares a status with older
errors. POINTER_FINAL, POINTER_FORKED and CONFLICT are 409s and no longer
read as AlreadyExistsError: they raise PointerFinalError,
PointerForkedError (a ForkError subclass) and ConflictError.
CLOSE_GROUP_SHORTFALL is a 503 raising CloseGroupShortfallError, whose
retryable flag says a pointer finalize kept the paid state. Over gRPC the
same classes come from the x-antd-error-code trailing metadata, on both
the sync RpcError and the aio AioRpcError, but only when the tag agrees
with the status code; retryable comes from the message's closing "(paid
attempt retained" hint. The mapping applies to every RPC, so a chunk store
whose close group fell short raises CloseGroupShortfallError too. The new
classes are additive, and the codes they catch come only from a daemon
with the pointer API. No write is ever retried; the README says each one
pays.

Stubs: pointers added to scripts/generate_proto.py, regenerated with the
pinned grpcio-tools 1.80.0 (protobuf gencode 6.31.1). The existing stubs
regenerate byte-identically.

Tests: 146 new unit tests over a mock REST daemon and an in-process gRPC
PointerService, sync and async: every method, each op shape prepare
sends, signed quotes, the error codes both ways with mismatched and
unknown tags, and counters up to u64::MAX and the malformed ones. The
suite is 509 passed, 8 skipped. tests/test_live_pointers.py is gated on
ANTD_TEST_URL, ANTD_TEST_GRPC and ANTD_TEST_POINTER_OWNERS. It passed
against a 10-node devnet over all four clients:
- create, update, get, resolve, with bytes fetched back through the
  pointer
- finality, and a gRPC create and get
- a transfer, then POINTER_FINAL over REST, gRPC and aio gRPC
- a prepare up to the quote, with CONFLICT while it is pending

examples/09_pointers.py (ANTD_POINTER_OWNER, optional ANTD_BASE_URL)
stores two values, creates a pointer at the first, updates it to the
second, then resolves it and prints its finality (open, 7/7 answered,
counter 1) against the same devnet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both clients, and IAntdClient, gain the daemon's pointer surface:
- pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
- pointerGet, pointerResolve, pointerFinality, pointerController
- pointerPrepare (with includeSignedQuotes) and pointerFinalize, for an
  external signer

The gRPC client talks to PointerService. pointers.proto is copied
byte-for-byte into lib/src/main/proto, so the kotlin-proto-copy drift
check passes; the 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) and nothing generated is committed.

Counters are ULong. The daemon sends them as decimal strings, because a
final state's u64::MAX is past what JSON numbers keep exactly in
several languages. Parsing is strict: ASCII digits only, then
toULongOrNull, so a sign, whitespace, a non-ASCII digit or an overflow
is an InternalException, never a zero. Finality status is a sealed
class with an Unknown(wire) arm for statuses a newer daemon may add.
REST nulls and gRPC's empty strings and absent messages both read as
null.

Errors map by the daemon's code where it diverges from the status.
POINTER_FINAL, POINTER_FORKED and CONFLICT are 409s and no longer read
as AlreadyExistsException; CLOSE_GROUP_SHORTFALL is a 503 whose
retryable flag says a pointer finalize kept the paid state. They are
four new exceptions: PointerFinalException, PointerForkedException
(which extends ForkException, now open), ConflictException and
CloseGroupShortfallException. Over gRPC they come from the
x-antd-error-code trailer, only when it agrees with the status code,
for every call rather than only pointer calls; the error wrapper now
passes StatusException.trailers to the mapper. Retryable is read from
the closing "(paid attempt retained" hint. Paid writes are never
retried. The exceptions are not sealed, so no exhaustive when breaks,
but IAntdClient gains ten abstract methods, so a caller's own
implementation of it must add them; the README's upgrade notes say so.

Tests: 49 unit tests for every method over a mock REST daemon and an
in-process gRPC server, covering the error mapping both ways, the
trailer/status agreement, counters up to u64::MAX, malformed counters
and single-send paid writes. LivePointersTest is a round trip against a
live daemon, gated on ANTD_TEST_URL, ANTD_TEST_GRPC and
ANTD_TEST_POINTER_OWNERS. It passed against a 10-node devnet:
- create, update, get, resolve, with bytes fetched back through the
  pointer
- finality, and a gRPC create, get and finality
- a transfer, then POINTER_FINAL over both transports
- a prepare up to the quote, with CONFLICT while it is pending and a
  finalize without payment refused over both transports

Example 08 (Example08Pointers.kt) uploads two public blobs, creates a
pointer at the first, moves it to the second, reads and resolves it
and prints its finality. It reads ANTD_POINTER_OWNER and ANTD_BASE_URL,
skips with a note without an owner, and is left out of "all" because
it pays. It ran clean against the same devnet: counter 0 then 1,
resolved to the second blob, finality open with 7/7 peers answering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AntdClient and GrpcAntdClient gain the daemon's pointer surface:
- pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
- pointerGet, pointerResolve, pointerFinality, pointerController
- pointerPrepare (a PointerPrepareRequest.create / .update / .transfer,
  with includeSignedQuotes) and pointerFinalize, for an external signer

The gRPC client talks to PointerService on its own channel. No write is
retried: every attempt pays, and the README and doc comments say so.

Counters are BigInt. The daemon sends them as decimal strings, because a
final state sits at 2^64 - 1 (pointerFinalCounter), past a Dart int and
far past the 2^53 a web build keeps exactly. They are parsed strictly:
anything but ASCII digits up to 2^64 - 1 throws InternalError rather
than reading as zero. Finality status is a sealed PointerFinalityStatus
(PointerOpen, PointerSettling, PointerUnconfirmed, PointerFinal,
PointerForked) with a PointerFinalityUnknown arm for statuses a newer
daemon may add. The record's flag is isFinal. Unset values, REST nulls
and gRPC "" or absent messages, all read as null.

Errors map by the daemon's code where it diverges from the status.
POINTER_FINAL is a new PointerFinalError, CONFLICT a new ConflictError
and POINTER_FORKED the existing ForkError, so these 409s no longer read
as AlreadyExistsError. CLOSE_GROUP_SHORTFALL is a new
CloseGroupShortfallError with retryable, a ServiceUnavailableError
subclass so existing 503 handlers still catch it. Over gRPC the same
types come from the x-antd-error-code trailer (GrpcError.trailers),
only when it agrees with the status code, so FAILED_PRECONDITION
without it is still a PaymentError; retryable comes from the message's
closing "(paid attempt retained" hint. AntdError is not sealed, so no
existing catch or switch stops compiling.

Stubs are regenerated with tool/generate_proto.sh (protoc_plugin
22.3.0, protoc 25.3), with pointers added to its PROTOS list. Without
it the existing stubs regenerate byte-identically. With it,
upload.pbgrpc.dart and wallet.pbgrpc.dart change only their import
alias ($6 to $7, $7 to $8): protoc_plugin numbers the aliases across
the one call in file-name order, and pointers sorts before them.

Tests: 82 new unit tests, 219 in all, passing. They cover every method
over a mock REST daemon and an in-process gRPC server, request bodies,
the error mapping both ways, and counters up to 2^64 - 1 alongside the
malformed ones. test/live_pointers_test.dart is a round trip against a
live daemon, skipped unless ANTD_TEST_URL, ANTD_TEST_GRPC and
ANTD_TEST_POINTER_OWNERS are set. It passed against a 10-node devnet:
- create, update, get, resolve, with bytes fetched back through the
  pointer, and AlreadyExists on a second create
- finality, and a gRPC create and get
- a transfer, controller, then POINTER_FINAL over both transports
- a prepare up to the quote, with CONFLICT over both transports while
  it is pending

example/09_pointers.dart (owner from ANTD_POINTER_OWNER, daemon from
ANTD_BASE_URL) ran against the same devnet: create at counter 0,
update at counter 1, resolve back to the second payload, finality open
with 7/7 peers answering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both clients gain the daemon's pointer surface, as new requirements on
AntdClientProtocol:
- pointerKeys, pointerCreate, pointerUpdate, pointerTransfer
- pointerGet, pointerResolve, pointerFinality, pointerController
- pointerPrepare(owner:op:options:) and pointerFinalize, for an external
  signer, with a protocol-extension pointerPrepare(owner:op:) for the
  default options

The models are PointerRecord (isFinal, since final is a Swift keyword),
PointerTarget, PointerFinality, PointerFinalState, PointerController,
PointerTransferResult, PointerWriteResult, PointerKey, PointerPrepareResult
and PointerFinalizeResult. The write kinds are the PointerKind and
PointerOp enums, and SignedQuoteEntry carries the signed quotes as base64
on both transports. PointerFinalityStatus is an enum with an
unknown(String) case for statuses a newer daemon may add. The gRPC client
talks to PointerService.

Counters are UInt64. The daemon sends them as decimal strings, because a
final state's UInt64.max is past what JSON numbers keep exactly in
several languages. Only ASCII digits that fit a UInt64 are accepted; an
empty, signed or malformed counter is an InternalError, not a zero.
Unset values map to nil on both transports: REST null, gRPC "" or an
absent message.

Errors map by the daemon's code where it diverges from the status.
POINTER_FINAL, POINTER_FORKED and CONFLICT are 409s and no longer read
as AlreadyExistsError: they map to PointerFinalError, PointerForkedError
(a ForkError subclass, so ForkError is no longer final) and
ConflictError. CLOSE_GROUP_SHORTFALL is a 503 and maps to
CloseGroupShortfallError, whose retryable flag says a pointer finalize
kept the paid state. Over gRPC the same errors come from the
x-antd-error-code trailer (RPCError.metadata), but only when it agrees
with the status code, so an untagged FAILED_PRECONDITION is still a
PaymentError. The gRPC retryable flag comes from the message's closing
"(paid attempt retained" hint; that tail parser is now shared with the
partial-upload mapping, whose behaviour is unchanged. The errors are
classes, not an enum, so no exhaustive switch breaks. Types that
conform to AntdClientProtocol themselves must add the new methods; the
README's upgrade note says so. No pointer write is retried, and the
README says each write pays.

Stubs: pointers.proto is added to scripts/generate-protos.sh. They were
regenerated on Linux with protoc 25.3, swift-protobuf 1.37.0 and
grpc-swift-protobuf 1.3.1 (grpc-swift 2.2.3); the existing stubs
regenerate byte-identically.

Tests: 48 new, 144 in all, passing. They cover every method over a
stubbed URLSession and an in-process gRPC server whose mock sends the
daemon's tagged statuses. They also cover error mapping both ways,
mismatched and missing tags, counters up to UInt64.max with malformed
ones refused, and that a failed paid write is sent exactly once.
LivePointerTests, gated on ANTD_TEST_URL, ANTD_TEST_GRPC and
ANTD_TEST_POINTER_OWNERS, passed against a 10-node devnet:
- create (with a 0x owner), AlreadyExists, get, resolve to bytes,
  update, finality
- a gRPC create, get and finality
- a transfer, controller, and POINTER_FINAL over both transports
- a prepare, with CONFLICT over both transports, and a refused finalize
  over both

The new example, `swift run AntdExamples pointers` (or 9), stores two
public targets, creates a pointer at the first, updates it to the
second, reads and resolves it back to the bytes, and prints its finality
("open", 7/7 peers, counter 1) against the same devnet. It reads
ANTD_POINTER_OWNER, plus ANTD_BASE_URL for the daemon URL, and is left
out of `all` because it pays and needs a fresh owner each run.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both clients gain the daemon's pointer surface, with bang variants, and
Antd delegates for the REST ones:
- pointer_keys, pointer_create, pointer_update, pointer_transfer
- pointer_get, pointer_resolve, pointer_finality, pointer_controller
- pointer_prepare (an op of {:create | :update, target[, kind]} or
  {:transfer, recipient}, with an include_signed_quotes option) and
  pointer_finalize, for an external signer

The gRPC client talks to PointerService. lib/antd/v1/pointers.pb.ex
comes from scripts/generate-protos.sh with pointers added to its list
(protoc-gen-elixir 0.16.0, protoc 25.3); the existing stubs regenerate
byte-identically. The gRPC pointer functions take a :timeout option that
defaults to five minutes, not grpc's ten seconds, since a write quotes,
pays and stores before it answers.

Counters are plain integers, which Elixir keeps exact at any size. They
are parsed strictly from the daemon's decimal strings: ASCII digits only
(Integer.parse would accept a sign or trailing text) and no more than
2^64-1. Anything else is an error, not a zero. Finality status is an
atom, with an {:unknown, status} arm for statuses a newer daemon may
add. Unset values read as nil on both transports.

Errors map by the daemon's code where it diverges from the status. The
new exception modules are PointerFinalError, ConflictError and
CloseGroupShortfallError (its retryable flag marks a pointer finalize
that kept the paid state); POINTER_FORKED maps to the existing
ForkError. POINTER_FINAL, POINTER_FORKED and CONFLICT are 409s and no
longer read as AlreadyExistsError, and CLOSE_GROUP_SHORTFALL's 503 is no
longer a ServiceUnavailableError. REST reads the body's code on every
call. Over gRPC the pointer functions read x-antd-error-code, trusted
only when it agrees with the status code, so FAILED_PRECONDITION without
it is still a PaymentError.

grpc-elixir's Gun adapter drops a failed call's metadata, so
GrpcClient.new now connects through Antd.GrpcAdapter. It is the Gun
adapter, except that a unary call made with return_headers: true
returns {:error, error, metadata} when it fails. It takes the call's gun
messages up to the end of the stream, notes their headers and trailers,
and hands them back for Gun to decode as usual. Every other call is
unchanged.

No pointer write is retried, and the docs say each call pays.

Tests: 85 new unit tests over a mock REST daemon (Bypass) and a mock
PointerService server. They cover every method, counters up to
u64::MAX, malformed counters, unset values, and the error mapping both
ways, including a tag sent trailers-only, a tag sent ahead of the
trailers, and a tag on the wrong status. The full suite passes (240
tests). test/antd/live_pointers_test.exs is a round trip against a live
daemon, gated on ANTD_TEST_URL and ANTD_TEST_POINTER_OWNERS, with gRPC
when ANTD_TEST_GRPC is set. It passed against a 10-node devnet over
REST and gRPC:
- create, update, get, resolve, with bytes fetched back through the
  pointer, and finality
- a gRPC create and get
- a transfer, then POINTER_FINAL over both transports
- a prepare up to the quote, with CONFLICT over both while it is pending

examples/08_pointers.exs (ANTD_POINTER_OWNER, optional ANTD_BASE_URL)
ran against the same daemon: two uploads, a create at counter 0, an
update to counter 1 that resolves to the second upload, and finality
open with 7 of 7 peers answering.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
examples/08-pointers.php read its daemon URL override from ANTD_URL.
antd-mcp, scripts/test-api.* and every other SDK's pointers example use
ANTD_BASE_URL, so the PHP example and its README entry now use that
name too. The default stays http://localhost:8082.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`ant dev example pointers -l <lang>` now runs the new pointers example
in all 15 SDKs.

`all` skips it. Each run pays from the daemon wallet and needs
ANTD_POINTER_OWNER set to an owner address whose key antd holds and
that has never been written, because a pointer is created once per
owner. A sweep that ran it would fail on the second run or spend an
owner each time. `all` prints how to run it on its own instead.
C#, Kotlin and Swift also leave it out of their in-SDK `all` modes,
which their adapters' `all` entries call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The MCP server gains ten tools over the antd SDK's pointer API, registered
next to the existing 19:
- reads, all free: pointer_keys, pointer_get, pointer_resolve,
  pointer_finality, pointer_controller
- daemon-wallet writes: pointer_create, pointer_update, pointer_transfer
- external signer, mirroring prepare_chunk_upload / finalize_chunk_upload:
  pointer_prepare and pointer_finalize

Every tool returns a JSON dict like the others. Counters go out as decimal
strings, never JSON numbers: a final pointer sits at 2^64-1, which clients
that read numbers as doubles, and LLMs, round. The tool descriptions say so.
pointer_get leaves out the owner's 1,952-byte public key unless asked
(include_owner_key), so a read does not spend about 3,900 characters of an
agent's context.

The writes keep the posture of store_data, upload_file and chunk_put: no
confirmation step, no cost pre-check, no retry. Their descriptions now say
that each call pays from the daemon wallet and that every repeat pays again.
A transfer is irreversible, so pointer_transfer goes further. Its
description opens with IRREVERSIBLE and it is annotated destructive and not
idempotent. Unless confirm_irreversible is true, it sends and pays nothing
and returns CONFIRMATION_REQUIRED, with a message naming both addresses.
Invalid kind or op strings are refused before anything is sent, as
payment_mode strings already are.

errors.format_error maps the pointer-era errors by exact type:
PointerFinalError to POINTER_FINAL, PointerForkedError to POINTER_FORKED
(not the VERSION_CONFLICT of its ForkError parent), ConflictError to
CONFLICT and CloseGroupShortfallError to CLOSE_GROUP_SHORTFALL with a
retryable flag. Each carries a hint saying what to do next. A retryable
shortfall reads "Retry the finalize, the payment was kept" and says to call
pointer_finalize again with the same upload_id. A non-retryable one says
not to repeat a paid write automatically. ALREADY_EXISTS on create is
unchanged.

The tools need an antd SDK with the pointer API, which is newer than 0.2.0.
The imports are guarded the same way as PartialUploadError's, and the tools
are registered only when the SDK's AsyncRestClient has pointer_get. Against
antd 0.2.0 from PyPI the server imports and lists its 19 tools as before,
and a 409 still reads as ALREADY_EXISTS. The dependency pin is unchanged;
pyproject notes what the pointer tools need.

Tests: 50 new tests, 94 in all, passing on mcp 2.3.0 and on the mcp 2.0.0
floor, with antd-py[rest] and with [all]:
- every tool over a mocked client, including argument forwarding, the typed
  kind and op, counters as strings (u64::MAX included), every finality shape
  (open, final, forked, unknown) and the transfer refusal without
  confirmation
- error mapping on mocked errors, and through the real AsyncRestClient over
  an httpx MockTransport for the 409 codes, a shortfall's retryable and a
  malformed counter, which reads as INTERNAL_ERROR
- the tool registry and annotations
- an import with the pointer API stripped from the SDK, which serves 19
  tools

Live smoke on the dev2 devnet daemon, through the in-memory MCP client
(lifespan, argument validation and ctx injection included):
- create and update on one owner gave counters "0" and "1". Get, resolve
  and finality ("open", counter "1") followed, and retrieve_data of the
  resolved target returned the second text.
- A second owner was created and transferred to the first. Without
  confirmation the call was refused and the pointer stayed untouched. With
  confirmation it went final 7/7, reading counter "18446744073709551615"
  with transferred_to set. Controller and resolve followed it.
- Updating or transferring it again returned POINTER_FINAL with its hint.
- pointer_prepare quoted an update (counter "2", wave-batch payments), and
  a write during it returned CONFLICT. An unknown upload_id finalized to
  NOT_FOUND.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…int for any store

The pointer tools take the server from 19 to 29 tools, and python-ci.yml's
registration step asserted exactly 19, so it would fail on this branch. It
now expects 29, message included.

The hint for a non-retryable CLOSE_GROUP_SHORTFALL told the agent to "check
the pointer" before paying again. The code also comes from a data or chunk
store whose close group fell short, where that reads wrong. The pointer
advice is now scoped ("For a pointer, check it with pointer_get or
pointer_finality first"). The rest keeps its meaning: a read is free to
retry, a paid write is not repeated automatically, and the user is asked
before paying again.

Tests: the non-retryable shortfall test asserts the scoped wording, and a new
test checks a chunk_put shortfall gets the same code and hint without the
pointer phrasing. 95 pass, in a fresh venv built with python-ci.yml's
install recipe (-e ../antd-py[rest] -e .[test]) and in the antd-py[all] dev
venv. The registration step, run verbatim from the workflow, reports 29
tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repo-level docs now cover the pointer API that antd and all 15 SDKs
gained. Pointers need the first antd release with pointers (after
0.15.0); no version number is named yet.

README.md lists pointers among the data primitives, says that data,
chunks and files stay immutable and that scratchpads, registers and
vaults are still unsupported, and adds a Pointers section: owner keys
from `ant pointer keygen` loaded by antd at startup via --pointer-keys,
every create, update and transfer pays and is never retried, transfer
is irreversible, and counters are decimal strings. A table gives each
SDK's pointer transport (gRPC in C++, C#, Dart, Elixir, Go, Java,
Kotlin, Python, Ruby, Rust and Swift; REST only in JS/TS, Lua, PHP and
Zig) and its example file. The MCP server's tool count goes from 19 to
29, with a line on its 10 pointer tools and the confirmation
pointer_transfer requires.

skill.md gains a mutable-primitive table, a pointers pattern, two key
rules (paid writes are never retried; transfer is irreversible) and the
pointer errors. llms.txt adds PointerService to the gRPC table, a short
Pointers section and the pointer error codes, and its MCP count goes
from 14 to 29. llms-full.txt adds a pointer block to every SDK's method
signatures, taken from each SDK's branch, plus the pointer model types,
the pointer prepare/finalize pair, the 10 MCP pointer tools, per-SDK
pointer error names and a runnable Python example.
docs/architecture.md drops "Immutable by design" for a Mutable
References section. Each of the 13 quickstarts gets a Pointers step
using that SDK's own method names, naming its example file and the
ANTD_POINTER_OWNER and ANTD_BASE_URL variables (the Rust example reads
only the first).

docs/tutorial-pointers.md walks through pointing at a public file and
updating it: generate a key, start antd with it, upload a file, create
the pointer, upload a second version and update it, resolve and fetch,
then read finality, with a line on each status. It explains transfer
and warns that it is irreversible, covers costs and the no-retry rule,
the external-signer prepare/finalize flow, and why counters are
strings. Python, C#, Kotlin and Swift are shown, as in the store and
retrieve tutorial, with the REST calls at the end.

The tutorial's REST walk-through ran as written against antd on the
10-node devnet: keys listed the owner, both file uploads, create at
counter 0, a second create refused with 409 ALREADY_EXISTS before
paying, update to counter 1, resolve to the second upload and its
download, get, finality open with 7/7 peers answering, and controller.
A prepare quoted one wave-batch payment, and an update while it was
pending answered 409 CONFLICT. jq is not on the devnet host, so its
filters were checked locally against the same daemon's responses. The
keygen output was checked with the ant CLI, offline. One owner
address was spent; nothing was transferred.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…sion

The release that ships pointers is not numbered yet. Say "the antd
release after 0.15.0", as the other SDK READMEs do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dates an existing pointer

The pointers examples now match the other SDKs' pointers examples in
two ways. They take the daemon URL from ANTD_BASE_URL and fall back to
the SDK default. The sibling examples are unchanged.

If a create is refused because the owner's pointer already exists, for
example from an earlier run, the example prints a one-line note, points
the pointer at the first target with an update instead, and carries
on. The daemon refuses that create before paying. Re-runs against the
same owner now work. The pays note says the fallback update pays too.

Both re-ran once against antd on a 10-node devnet with ANTD_BASE_URL
set and the owners from their first runs. Each took the already-exists
path: an update to counter 2, then counter 3. Get and resolve pointed
at the second target and its bytes were fetched back. Finality was
open, with 7/7 peers answering. gofmt, go vet, golangci-lint v1.64.8,
cargo fmt --check and cargo clippy --all-targets -D warnings are clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…menters keep compiling

The ten pointer methods were abstract on IAntdClient, so any class of a
caller's own that implements the interface, such as a test fake, would
have stopped compiling on upgrade. Each now has a default body that
throws UnsupportedOperationException naming the method ("pointerGet is
not implemented by this IAntdClient"), as antd-csharp does.
AntdRestClient and AntdGrpcClient keep their real overrides; the
existing REST and gRPC pointer tests call all ten on each client, so a
missing override would fail them.

The defaults are Kotlin interface bodies under the compiler's default
-Xjvm-default mode, so they cover Kotlin implementers. A Java class
implementing IAntdClient still has to declare the methods, as it does
for every other member today.

A new test implements IAntdClient without any pointer method, which
compiles, and checks that all ten throw with that message. The README's
upgrade notes now say the interface change is not breaking. They keep
the one behaviour change: a daemon with the pointer API reports a close
group shortfall on any call as CloseGroupShortfallException, where an
older one sent a 500 read as InternalException.

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

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… conformers keep compiling

The ten pointer methods stay requirements on AntdClientProtocol, so
AntdRestClient and AntdGrpcClient still dispatch to their own
implementations through the protocol. A protocol extension now gives
each one a default implementation that throws the new NotSupportedError,
so a type that conformed to the protocol before the pointer API
(a test double, say) compiles unchanged. NotSupportedError is a small
AntdError subclass with statusCode 0, because the error is raised on
the client side and nothing in the existing hierarchy fits it. The
pointerPrepare(owner:op:) convenience still routes through the
requirement.

The protocol change is therefore no longer breaking. One behaviour
change remains and the README still documents it: CLOSE_GROUP_SHORTFALL
maps to CloseGroupShortfallError on any path.

Tests: a conformer that implements every requirement except the pointer
methods compiles, and each of its pointer methods throws
NotSupportedError naming the method. Both real clients, called as
`any AntdClientProtocol`, reach their own implementation for all ten
methods, so a signature drift cannot fall through to a default
unnoticed. swift test on dev2: 147 tests, 1 skipped (the env-gated live
test), 0 failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The REST client gains the daemon's manifest surface (ADR-0006 `.ant`
files and `ant://` links), in a new antd.manifests module that installs
its methods on Client:
- build_manifest(entries, opts): POST /v1/manifests/build
- decode_manifest(bytes, resolve) and decode_manifest_link(link,
  resolve): POST /v1/manifests/decode

Each entry is { path, size, data_map, address } with exactly one of
data_map (embedded) or address (public). Both or neither is a
bad_request before anything is sent, as antd-go's checkManifestEntries
does, and so is a malformed size, path or option. "" counts as unset,
as it does in the daemon. opts is { name, torrent_info_hashes,
embed_roots }; embed_roots nil means true, so the default embeds roots.
The result tables carry antd-go's fields: BuiltManifest { manifest,
link, entries }, DecodedManifest { source, name, torrent_info_hash_v1,
torrent_info_hash_v2, entries }, and per entry effective_name, path,
size, kind, content_address, data_map, exact_size and error.
ManifestEntryKind and ManifestSource hold the wire strings.

The `.ant` bytes are a Lua string both ways; base64 is only the wire
form. The response's base64 is checked before it is decoded (alphabet,
padding, length), since mime.unb64 skips unknown symbols and drops an
unpadded tail. Anything else there is an internal error.

Sizes are Lua numbers, exact up to 2^53 (MAX_MANIFEST_SIZE). Unset reads
as nil, never 0, and so do the optional strings (path, data_map, error,
link, name, the torrent hashes), where antd-go reads "". On Lua 5.3+ a
size reads as an integer. The build body is assembled by hand rather
than by cjson.encode: cjson writes numbers with 14 significant digits,
which would round a size from 10^14 up, and it writes an empty table as
{} where the daemon wants an empty entries array (a 422). Sizes go out
with %.0f, exact to 2^53; strings still go through cjson. _do_json
accepts such a pre-encoded string body.

A 400 is the existing bad_request, with the daemon's message (it names
entries[i], counting from 0). One entry's resolve failure stays in its
error field and the call succeeds. No new error types.

Tests: spec/manifests_spec.lua has 26 busted cases over mocked HTTP:
the request bodies, the base64 round trip over every byte value and a
malformed response, unset and null sizes as nil and 0 as 0, sizes past
14 digits, the both-or-neither check, the embed_roots default, the empty
entry list, and the 400 mapping. The full suite passes under Lua 5.1,
LuaJIT and Lua 5.4 (161 successes, 1 pending), against the worktree
source. luacheck is clean on the new files. spec/live_manifests_spec.lua
is pending unless ANTD_TEST_URL and ANTD_TEST_MANIFEST_ADDRESSES are set.
Against a 10-node devnet it passed under all three, paying nothing:
- a build of two public addresses and a DataMap, with kinds, effective
  names, content addresses, a recorded size and an unset one, and a link
- decodes of the bytes and the link with resolve, exact sizes matching
  the fixtures and the DataMap entry embedded with its data_map
- a bare address as a file link, and an unknown one whose resolve fails
  in the entry's error
- the fixture link, and bad_request for both sources (before sending)
  and for a "../x" path (the daemon's 400, naming entries[1])

examples/09-manifests.lua uploads two pieces of public data, builds a
manifest of them as notes/one.txt and notes/two.txt named "example",
prints the link and entries, and decodes the link with resolve (exact
sizes 26 and 43 bytes). It ran once against the same devnet under Lua
5.4. ANTD_BASE_URL overrides the daemon URL.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RestClient gains antd's ADR-0006 manifest surface, mirroring antd-go:
- buildManifest(entries, { name?, torrentInfoHashes?, embedRoots? })
  -> BuiltManifest { manifest, link, entries }
- decodeManifest(manifest, resolve = false) and
  decodeManifestLink(link, resolve = false) -> DecodedManifest
  { source, name, torrentInfoHashV1, torrentInfoHashV2, entries }
- ManifestEntryInput { path?, size?, dataMap?, address? } and
  ManifestEntryInfo { effectiveName, path, size, kind, contentAddress,
  dataMap, exactSize, error }

The types and wire conversion live in a new src/manifests.ts; RestClient
only adds the three methods after the Files section, and index.ts exports
the types. The SDK is REST-only, so there is no gRPC side.

The `.ant` bytes are a Buffer both ways, and decodeManifest also takes a
plain Uint8Array (encoded as a view, not copied). REST carries them as
standard base64 in `manifest`; callers never see it. A build response
whose manifest is not padded standard base64 throws InternalError rather
than letting Node's lenient decoder return the wrong bytes.

Sizes are number, the SDK's size type. Unset is null, never 0: REST
omits an unset size or exact_size, and both read as null, while a 0 is
sent and read as 0. A size the daemon sends that is not a non-negative
integer throws InternalError. Every other unset value is null as well:
path, name, the torrent hashes, error, the link when it would pass 1,500
bytes, and dataMap on public entries and on every built entry.

embedRoots defaults to true and is always sent as embed_roots, so an
empty options object embeds roots. An entry with both dataMap and
address or neither (an empty string counts as unset), or a size that is
not a non-negative safe integer, throws BadRequestError naming
entries[i] before anything is sent, as antd-go's checkManifestEntries
does; so do empty manifest bytes and an empty link. The daemon's 400 is
the existing BadRequestError with its message kept, and a 404 or 502
while resolving a shrunk DataMap keeps its NotFoundError or NetworkError.
No new error classes. Nothing here uploads or pays; the README says so,
and that build and resolve read the network and are safe to retry.

Tests: 32 unit tests over a mocked fetch in src/manifests.test.ts cover
the wire body, the base64 round trip (including + and /, a Uint8Array
view and malformed base64), unset sizes reading as null with 0 kept, the
both-or-neither check and size check before sending, the embed_roots
default, a file link decode, and 400/404/502 mapping. The suite is 140
passing, with typecheck and build clean on Node 18. The example also
typechecks against a scratch tsconfig.

src/live-manifests.test.ts is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES. Run on its own against a manifest-enabled
daemon on a 10-node devnet, it passed 4 of 4: a build from the two
fixture addresses and a DataMap (kinds, sorted effective names, content
addresses, null sizes, the embedded root's exact size, a link); resolving
decodes of the bytes and the link agreeing, with the fixture sizes and an
embedded DataMap that dataGet downloads; the fixture link decoding, and
rebuilding it from its decoded entries giving the same link; and
BadRequestError for both sources, for "../x" (the daemon's message names
entries[1]) and for bytes that are not a manifest.

examples/09-manifests.ts (optional ANTD_BASE_URL) ran clean against the
same daemon: two public uploads, a 184-byte manifest named example with a
link, notes/one.txt and notes/two.txt resolving to 31 and 48 bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
AntdClient gains the daemon's ADR-0006 manifest surface, each with an
Async variant like the rest of the client:
- buildManifest(entries, ?BuildManifestOptions) over
  POST /v1/manifests/build
- decodeManifest(bytes, resolve = false) and
  decodeManifestLink(link, resolve = false) over
  POST /v1/manifests/decode

The methods live in a ManifestMethods trait that AntdClient uses, so the
client class itself changes by one line and the pointer methods appended
to it merge without a conflict. The models are readonly classes:
ManifestEntryInput, BuildManifestOptions, ManifestEntryInfo,
BuiltManifest and DecodedManifest. BuildManifestOptions takes name,
torrentInfoHashes and embedRoots, which defaults to true, so no options
and `new BuildManifestOptions()` both send `embed_roots: true`. The
client always sends it, as antd-go does.

The `.ant` bytes are raw PHP strings in and out. The client
base64-encodes them into the decode request and strictly decodes the
build response's `manifest`. A response that is not valid base64 throws
UnexpectedValueException, so the caller never sees base64.

Sizes are ?int, PHP's 64-bit signed int, which is ample for file sizes.
Unset is null, never 0: a null input size is left out of the request,
and `size` / `exact_size` read as an int only when the JSON holds a
non-negative integer. A missing field or JSON null reads as null. The
other optional fields (link, path, data_map, error, name, the torrent
info hashes) read as null when the daemon leaves them out or sends an
empty string.

Each entry must set exactly one of dataMap or address. Null and '' both
count as unset, as antd-go treats ''. buildManifest checks this, and a
negative size, before sending, and throws InvalidArgumentException
naming entries[i]. It does the same for an element that is not a
ManifestEntryInput. A keyed entries array is still sent as a JSON list.
An empty manifest or link is refused before sending too. The Async
variants throw these synchronously, before a promise exists. There are
no new error codes. The daemon's 400 is the existing BadRequestError
with the daemon's message (`entries[i]: ...`) kept, and missing wrapper
records on build are a NotFoundError. One entry's resolve failure lands
in that entry's `error`, not in an exception. This is additive: nothing
existing changes shape. PHP has no gRPC client, so there is no receive
limit to raise.

Tests: 29 unit tests over a Guzzle MockHandler. They cover the request
bodies, the base64 round trip of all 256 byte values, absent, null,
zero and garbled sizes (only a real integer is a size, and 0 stays 0),
the embed_roots default, the both-or-neither check (sync and async,
with nothing sent), the empty-input checks, and 400 and 404 mapping to
BadRequestError and NotFoundError. The suite runs 131 tests, all
passing with one skip, plus composer validate --strict and php -l over
src, tests and examples, as CI does, on PHP 8.3.

tests/LiveManifestsTest.php is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES and makes no paid calls. It passed against
a 10-node devnet:
- a build from the two fixture addresses and the fixture DataMap, with
  paths: kinds, effective names in sorted order, the header and a link
- resolving decodes of the bytes and of the link: exact sizes 46 and 85
  and 82, and the DataMap entry embedded with its data_map
- a non-resolving decode leaving the public exact sizes null
- a bare address decoded as a file_link, and the fixture link decoded
- `../x` refused as a BadRequestError naming entries[1], garbage bytes
  refused, and both sources refused before sending

examples/09-manifests.php (optional ANTD_BASE_URL) ran against the same
daemon. It uploaded two public files and built a 184-byte manifest
named `example` with notes/one.txt and notes/two.txt. It printed the
link and both entries, and decoding the link with resolve gave 36 and
42 bytes, matching the uploads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
antd-go already has the manifest API (BuildManifest, DecodeManifest,
DecodeManifestLink on both clients). This brings it up to the bar the
other SDKs are held to. The API is unchanged.

examples/09-manifests reads the daemon URL from ANTD_BASE_URL (default
antd.DefaultBaseURL). It stores two small pieces of public data, builds
a manifest named "example" with paths notes/one.txt and notes/two.txt,
prints the link and each entry's effective name and content address,
then decodes the link with resolve and prints each entry's exact size.
It says up front that only the two uploads pay. The number follows the
pointers example (08), so the two never collide. The README lists it.

integration_manifests_test.go is a live test that pays for nothing. It
skips unless ANTD_TEST_MANIFEST_ADDRESSES is set, then takes the daemon
from ANTD_TEST_URL through integrationClient, as the pointer test does.
It builds a manifest of the two fixture addresses and the fixture
DataMap, all with paths. One size is recorded and two are left unset.
It checks the entries' order, kinds, effective names, content addresses
and the link. An unset size reads back as nil and an embedded root
carries its exact size. It then decodes the bytes and the link with
resolve: every exact size matches the fixtures, and only the DataMap
entry, kind embedded, carries a non-empty DataMap. Without resolve, the
public entries have no exact size. It decodes ANTD_TEST_MANIFEST_LINK
and matches its entries by content address and size. An entry with the
path ../x is a BadRequestError whose message names entries[1]. With
ANTD_TEST_GRPC set, the same checks run over gRPC, and the gRPC build
must give REST's exact bytes and link.

A new unit test pins the gRPC receive limit. A mock Build response and
a mock Decode response, each 1 MiB past the client's 32 MiB default
ceiling, arrive on a default client. Lowering the per-call
MaxCallRecvMsgSize to the default makes it fail with a 413.

The manifest doc comments said "Requires antd >= 0.16.0". They now say
"Requires the antd release after 0.15.0", as the pointer README does.
The README gains a Manifests section. It covers what the three calls
do, that nothing is uploaded or paid for, raw bytes with base64 only
on the REST wire, *uint64 sizes where nil means unset, the embed-roots
default, per-entry resolve errors and retries. It also notes that
gRPC raises the receive limit to 256 MiB per manifest call,
independent of WithGrpcMaxRecvMsgSize, and gives a short code sample.

Verified against the manifest-enabled antd on a 10-node devnet:
- the live test passes over REST and gRPC (457-byte manifest,
  625-byte link, identical on both transports), and fails as expected
  with a wrong fixture size or an unreachable gRPC target;
- the example ran once: two uploads, a 184-byte manifest, both
  entries resolved to 26 and 43 bytes;
- go vet, gofmt, go mod verify and golangci-lint v1.64.8 are clean;
  go test passes with and without -race (135 pass; the 10
  integration tests skip without a daemon).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
AntdClient, AsyncAntdClient and GrpcAntdClient gain the daemon's ADR-0006
manifest surface, with the same models and exceptions on all three:
- buildManifest(entries[, options])
- decodeManifest(bytes, resolve)
- decodeManifestLink(link, resolve), for a manifest link, a file link or
  a bare address

The models are records: ManifestEntryInput (ofDataMap / ofAddress, with
withPath / withSize), BuildManifestOptions (name, torrentInfoHashes,
keepShrunkDataMaps), ManifestEntryInfo, BuiltManifest and
DecodedManifest. As in antd-go, root embedding is the default:
keepShrunkDataMaps is false in BuildManifestOptions.DEFAULT and in a null
options argument, and embed_roots = !keepShrunkDataMaps is always sent.
The wire shapes live in a new ManifestWire, beside PointerWire, so the
client classes only gain a short, self-contained section each.

The public API takes and returns raw byte[]. REST carries the .ant bytes
as base64 in JSON (the build response's and the decode request's
manifest), encoded and decoded inside the client; a build response whose
manifest is not valid base64 is an AntdException. gRPC carries them raw.

Sizes are Long, as the SDK's other 64-bit sizes are signed longs: unset
is null, never 0. REST omits unset fields and the client reads an absent
or null size as null; gRPC reads proto3 optional fields by presence
(hasSize, hasExactSize), so a recorded 0 stays 0. An unset path, link,
name, torrent hash, data_map or error is null on both transports, as
elsewhere in the SDK.

Before anything is sent, an entry with both or neither of dataMap and
address, a null entry or a negative size throws IllegalArgumentException
naming entries[i], as antd-go's checkManifestEntries does; so do empty
decode input and a null entry list. The daemon's own rejections need no
new code: a 400 is BadRequestException and INVALID_ARGUMENT already maps
to it, with the daemon's entries[i] message kept. One entry's resolve
failure is its error field, not a failed call.

gRPC limits: every manifest call builds its stub with
withMaxInboundMessageSize(256 MiB), antd-go's ceiling, since a manifest
can be 64 MiB and a decode response adds its DataMaps in hex, and
withMaxOutboundMessageSize(100 MiB), the daemon's request limit. The
stub is made per call, so the channel's other calls keep the defaults.
The stubs come from the existing protobuf Gradle build (protoc 4.28.3,
protoc-gen-grpc-java 1.68.0), which already compiles every proto under
antd/proto, so nothing generated is committed.

Tests: 33 new unit tests, all passing with the existing 145.
ManifestRestTest covers both REST clients against MockWebServer: the
request shapes, the base64 round trip over all 256 byte values, unset
sizes as null and 0 as 0, a size past int range, the embed_roots
default, the checks before sending, and a 400 as BadRequestException.
GrpcManifestTest covers an in-process ManifestService: field presence
both ways, raw bytes, INVALID_ARGUMENT, and the limits on every call
(recorded by an interceptor). Over a real socket a 5 MiB manifest
arrives, and a decode sends it back, while a plain stub on the same
channel is refused it. Example09ManifestsTest drives the example
against a mock daemon.

LiveManifestsTest is gated on ANTD_TEST_URL and
ANTD_TEST_MANIFEST_ADDRESSES and pays nothing. It passed against the
10-node devnet behind the manifest-enabled antd:
- a build from the two public addresses and the private DataMap, with
  paths, a name and one recorded size: kinds, effective names, content
  addresses and a link
- resolving decodes of the bytes and of the link: exact sizes 46 and 85,
  and 82 for the embedded entry, with its DataMap
- the fixture link, a bare address as a file link, and an unresolved
  decode leaving exact sizes unset
- BadRequestException for a "../x" path and for bytes that are not a
  manifest, and IllegalArgumentException for both sources
- the async build, and the same over gRPC, with byte-identical
  manifests and equal decodes on both transports

Example09Manifests uploads two pieces of public data, builds a manifest
of notes/one.txt and notes/two.txt named "example", prints the link and
each entry's content address, then decodes the link with resolve.
Against the devnet it built 184 bytes with a link, and printed exact
sizes of 17 and 34 bytes. It reads ANTD_BASE_URL.

The README gains a manifests section, which says nothing is uploaded or
paid for and that the methods need the antd release after 0.15.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman and others added 3 commits October 16, 2026 17:28
…ls (#342)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…L went unpaid

ant-core pays for a pointer write before storing it, and the store can
still fail. An update that loses to a newer state while in flight comes
back as CONFLICT after paying, and a pointer that became final in flight is
refused as POINTER_FINAL after paying. The hints said "Nothing was paid" for
both, which could lead an agent to pay for another write on false cost
information.

- CONFLICT's hint now follows the daemon's message: a write refused because
  a prepared write awaits payment says it was refused before paying; an
  update that lost to a newer state says it may already be paid for and to
  ask the user before paying again; anything else claims neither.
- POINTER_FINAL's hint says it is usually refused before paying, but a
  write already paid for keeps that payment.
- README and the tool docstrings say the same.
- Tests: the newer-state CONFLICT must not claim nothing was paid; the
  prepared-write CONFLICT says it was refused before paying; an unrecognised
  CONFLICT message claims neither.

Found by the review of #342.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@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 — reviewed integration only. DO NOT MERGE.

Repository: WithAutonomi/ant-sdk. Reviewed exact head bc339abe3e8914104139cf63253913892ed28432 against ea389c79c2ac40601c574e93031a66a9791db1d2. This is the standalone integration review of #367, not a new line-by-line review of every language binding. No new severe integration defect was found in the reviewed composition, conflict-resolution hunks and integration-specific fixes. This approval does not authorise a merge, release, payment or live-network operation.

Panel participation and limits

9/12 full-bundle reviews completed — partial panel, not full consensus. GLM 5.3 (z-ai/glm-5.3) and local Qwen 3.8 (qwen38-27b) completed both independent roles for both integration scopes. Local DS4 (deepseek-v4-flash) completed the full-bundle scope-1 adversarial role. Other full-bundle DS4 responses terminated at the output limit and were not counted. Each missing DS4 seat received one narrowed same-model retry, then stopped; no fallback is counted as DS4.

  • Integration scope 1: 5/6 full-bundle seats substantive. glm: correctness/adversarial, qwen: correctness/adversarial, ds4: adversarial.
  • Integration scope 2: 4/6 full-bundle seats substantive. glm: correctness/adversarial, qwen: correctness/adversarial.
  • DS4 scope 1 correctness: original full-bundle result unavailable; one narrowed retry substantive (APPROVE).
  • DS4 scope 2 correctness: original full-bundle result unavailable; one narrowed retry substantive (APPROVE).
  • DS4 scope 2 adversarial: original full-bundle result unavailable; one narrowed retry unavailable (finish_reason=length).

Narrowed scope-1 retry: complete current MCP server.py and exact delta from parent, with AST/exact-snippet equivalence checks and composition facts. Narrowed scope-2 retries: all original production/build/test merge-resolution and fix hunks; repeated README/llms documentation hunks explicitly excluded. Those narrower opinions are supplemental, not additional full-bundle votes. All substantive opinions support approval; no unresolved material dissent. Required partial-panel quorum is met independently by GLM and Qwen in both roles and scopes.

Verified at this exact head

  • 343 changed files; 37 first-parent merges and three nonmerge fixes. All 36 recorded included PR heads are ancestors and still match GitHub. Every conflict-free merge tree matches git merge-tree; tracked checkout is clean. Twenty-seven conflicted file occurrences were inventoried. #347 is excluded. antd/src/pointers.rs is unchanged from base.
  • All 36 included PRs have current-head formal approvals on GitHub. Earlier evidence described #335/#342 as changes-requested and #349/#350/#358/#361 as provisional; that was stale. I read back their new approvals and current heads. This does not remove any limitations in those individual reviews.
  • Fresh offline MCP pytest: 122 passed, 2 skipped. Fresh Python SDK pytest tests/: 584 passed, 12 skipped. Imports resolve to this checkout; 31 MCP tools, including pointers and manifests, register. All 15 ant-dev adapters expose both as opt-in examples.
  • A separate parent-run synthetic missing-feature matrix passed: 31 tools normally, 21 without pointer exports, 29 without manifest exports, and 19 without either. This checks conditional registration on the current checkout only; it is not an installation test of a real old SDK and is not counted as a panel opinion.
  • Broader unscoped Python pytest is not green: 584 passed, 12 skipped, six fixture errors from scripts/test_grpc.py and scripts/test_rest.py. Both helper files are byte-identical to base; the scoped unit suite passes. This inherited collection issue is recorded, not concealed as a pass.
  • GitHub CI at the reviewed head: 23 successful, 2 skipped. The skipped jobs are Go race detection and the Go security scan.

Remaining limits / human next steps

  • DO NOT MERGE this integration-preview PR on the strength of this approval. Decide how to land the reviewed component PRs separately.
  • Current CI has no job for Java, C++, Lua, Zig or ant-dev on this combined tree. Kotlin, Elixir, Ruby and Swift have proto-drift checks only, not SDK test suites. No combined-tree full SDK matrix or live-daemon/network test was performed here. Earlier component-only tests are not represented as combined-tree evidence.
  • Paid pointer/manifest examples were not executed. The normal all selection keeps these opt-in. No funds were spent or live network changed.
  • Older SDKs can expose a reduced MCP tool surface; the 31-tool check is for this current SDK, not every released SDK. Go race/security coverage and real daemon integration remain unverified here.
  • Full per-language source reviews remain the linked component PR reviews. This integration approval does not silently upgrade their partial panels to full consensus.

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

REQUEST_CHANGES — JavaScript manifest sizes can silently change

Repository: WithAutonomi/ant-sdk. Reviewed head: bc339abe3e8914104139cf63253913892ed28432. SDK source comparison: merge-base 532ae15cc2d9595e97d9162b9cfe741958b54653.

This is a broader SDK-source follow-up to my integration-only approval at this same head. The new finding below supersedes that approval as my current formal verdict. That earlier review explicitly excluded a fresh per-binding source review. This is not a claim that the merge composition lost a fix.

DO NOT MERGE this integration-preview PR. This review does not authorise a merge, release, payment or live-network operation.

Required fix — P2: reject or preserve unsafe manifest sizes

Location: antd-js/src/manifests.ts:139–142, used for both size and exact_size by entryFromJson at lines 226–235.

optionalSize accepts any non-negative Number.isInteger value. That is not enough after JSON parsing. The unchanged daemon's ManifestEntryInfo uses Serde Option<u64> for these fields (antd/src/types.rs:909–933), so the REST response can contain integer literals that JavaScript cannot represent exactly. The recorded size is a creator's claim; reproducing this case does not require a real multi-petabyte file.

I exercised the built public RestClient.decodeManifestLink against a controlled loopback HTTP server, without a live daemon:

wire size:         9007199254740993
returned size:     9007199254740992
error:             none

The same controlled case reproduces for exact_size. This is silent wrong output on a contract-valid integer, not a malformed-response hypothesis. The manifest module is new relative to the merge-base. The build-input path already uses Number.isSafeInteger at line 164, but the response path does not.

Minimum fix: use Number.isSafeInteger in optionalSize and throw InternalError when the response cannot be represented safely. Alternatively, expose an exact string/bigint representation, with lossless parsing of the original response text. A parser redesign is needed for exact preservation, not just for explicit rejection.

Required regression: use raw HTTP response text with size:9007199254740993, then repeat for exact_size. Assert an explicit error or an exact representation, never a rounded success. Keep positive checks for 0, 9007199254740991, and absent/null sizes. A scratch-only version of the built module with the safe-integer guard passed these controls: unsafe sizes throw InternalError; the safe/null cases stay unchanged. The PR branch was not edited.

Severity is P2 / data integrity. Very large sizes are unusual. I did not demonstrate payment loss or a live-network fault.

Non-blocking follow-ups

  • C# optional-feature parity: PointerPrepareRequest/PointerPrepareResult and both transports omit include_signed_quotes/signed_quotes, although the proto and other bindings expose them. Add the option and result, or document the cut. The core prepare/finalize path is not shown broken.
  • README: the text near lines 442 and 473 says Go/Rust examples do not read ANTD_BASE_URL; their new pointer/manifest examples do. Remove that exception.
  • C# REST 409 error typing remains a portability caveat. The status mapping predates this PR, and the new interface documents the legacy behaviour. I did not count it as a new blocker.

Review team and adjudication

6/6 focused role reviews completed: GLM 5.3 (z-ai/glm-5.3), local Qwen 3.8 (qwen38-27b) and local DS4 (deepseek-v4-flash), each in correctness and adversarial/cross-language roles. These were focused evidence packets, not six full line-by-line reviews of all 343 files. Separate read-only audits covered the native, dynamic-language and systems/docs groups.

All three models support the JavaScript size finding in the adversarial/cross-language pass. No unresolved dissent remains on that finding. Initial suggestions about out-of-range daemon tags, beyond-u64 protobuf counters, empty optional source fields and new retention flags were not accepted: the current daemon types/normalisation and unchanged error contract did not establish those as new valid-input bugs. One review favoured raw-text parsing; the parent-executed guard control confirmed that explicit rejection is the smaller sufficient fix. Optional C# quotes and the README issue remain non-blocking.

Executed checks and limits

  • Python SDK: scoped pytest tests/: 584 passed, 12 skipped. Broader pytest -q: the same passes/skips plus six fixture errors in scripts/test_grpc.py and scripts/test_rest.py. Those helper files are unchanged from merge-base; the broad run is not described as green.
  • MCP: 122 passed, 2 skipped. A separate installation smoke test registered 31 tools.
  • JavaScript: typecheck and ESM/CJS/declaration build pass; 202 tests passed, 5 skipped. The controlled precision-loss probe and scratch-only guard controls also pass as described above. Passing the reproduction means the bug was observed, not fixed in this branch.
  • Go: parent reran GOPROXY=off go test ./... with an isolated cache: pass.
  • GitHub CI at this head: 23 successful, 2 skipped. Go race detection and Go security scanning were skipped.
  • Zig: no local test pass. Zig 0.14.1 could not link its build runner against this macOS environment; the installed 0.16 toolchain was also incompatible with the package APIs. These are gaps, not proved PR defects.
  • 343 changed paths were inventoried and checked against GitHub's file count. Source audits were bounded; generated stubs were not all regenerated locally, and the complete native/dynamic SDK runtime matrix was not run. Elixir source coverage was targeted rather than line-by-line. No live-daemon, paid-example or network end-to-end run was performed.

Next step: fix the JavaScript response-size guard and add the raw-wire regression test. Keep the combined preview unmerged; landing the component changes remains a separate maintainer decision.

Comment thread antd-js/src/manifests.ts
*/
function optionalSize(field: string, value: unknown): number | null {
if (value === undefined || value === null) return null;
if (typeof value === "number" && Number.isInteger(value) && value >= 0) return value;

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.

[P2] Reject unsafe manifest response sizes instead of silently returning a rounded value. After JSON parsing, the valid u64 literal 9007199254740993 reaches this check as 9007199254740992, which still satisfies Number.isInteger. I reproduced this through the built public RestClient with a controlled HTTP response, for both size and exact_size. Use Number.isSafeInteger and throw InternalError, or adopt a lossless string/bigint representation. Add raw-response tests for the unsafe value plus safe/zero/null controls. A scratch-only safe-integer guard passed those controls; no PR branch edit was made.

Nic-dorman added a commit that referenced this pull request Oct 19, 2026
The daemon sends manifest entry `size` and `exact_size` as u64 JSON
numbers. JSON parsing rounds any integer past Number.MAX_SAFE_INTEGER, so
`optionalSize`, which only checked Number.isInteger, returned
9007199254740993 as 9007199254740992 with no error. The recorded size is
the creator's claim, so no real petabyte file is needed to hit it.

Accept only safe integers, and throw InternalError naming the field for
anything larger, as the build-input path already does. Regression: a raw
response body (not JSON.stringify, which would round first) with
9007199254740993 in `size`, then in `exact_size`, must throw. 0,
9007199254740991, and absent or null sizes keep reading exactly.

Requested by Hermes on #367 (P2, data integrity).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
main now has the manifest endpoint docs (#346) and the 16 pointer SDK
PRs. The one conflict is llms-full.txt's Go method list, where #339 adds
the pointer methods and #346 added the manifest methods at the same
spot. Kept both, pointers first, as in the integration preview (#367).
The merge's diff against main equals #339's own diff (19 files,
+1214/-11), so nothing else changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-php/README.md
The result matches #367 for antd-php byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-lua/README.md
The result matches #367 for antd-lua byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
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 added a commit that referenced this pull request Oct 19, 2026
main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-go/README.md
The result matches #367 for antd-go byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
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 added a commit that referenced this pull request Oct 19, 2026
main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-dart/CHANGELOG.md antd-dart/README.md antd-dart/example/example.md antd-dart/tool/generate_proto.sh
Also carries the #367 fix-up for this SDK: antd-dart/lib/src/generated/antd/v1/pointers.pbgrpc.dart antd-dart/lib/src/generated/antd/v1/upload.pbgrpc.dart antd-dart/lib/src/generated/antd/v1/wallet.pbgrpc.dart
The result matches #367 for antd-dart byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-py/scripts/generate_proto.py antd-py/src/antd/_grpc.py
The result matches #367 for antd-py byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
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 added a commit that referenced this pull request Oct 19, 2026
main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-rust/Cargo.toml antd-rust/README.md
The result matches #367 for antd-rust byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): antd-elixir/README.md antd-elixir/scripts/generate-protos.sh
The result matches #367 for antd-elixir byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
main now has the pointer fan-out. Conflicts resolved exactly as in the
integration preview #367 (bc339ab): ant-dev/src/ant_dev/cli.py ant-dev/src/ant_dev/cmd_example.py
The result matches #367 for ant-dev byte for byte.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
…365)

#365 landed on main after the previous merge, so the shared docs
conflicted again: README.md, docs/architecture.md, llms-full.txt,
llms.txt and skill.md. Resolved exactly as in the integration preview
#367 (bc339ab), which merged #365 before #339 too. Every doc file #339
or #365 touches now matches #367 byte for byte, including #367's
fix-up lines in README.md and llms.txt that describe pointers and
manifests together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nic-dorman added a commit that referenced this pull request Oct 19, 2026
Now that the py manifest API (#356) is on main. Conflicts in python-ci.yml and antd-mcp/README.md resolved as on the #367 preview (bc339ab), with the MCP tool-count tests from there (31 tools, 21 error codes).
@Nic-dorman
Nic-dorman merged commit bc339ab into main Oct 19, 2026
25 checks passed
@Nic-dorman
Nic-dorman deleted the nic/stack-integration-preview branch October 19, 2026 16:48
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