Repository navigation
feat(antd-java): pointer API (REST + gRPC) - #331
Conversation
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>
dirvine
left a comment
There was a problem hiding this comment.
Formal review: APPROVE
Repository: WithAutonomi/ant-sdk, PR #331.
Reviewed head: 5bade9b6565a4f758b48fe081567b06b7a4d752c. Original merge base: 8e67c611c8f54d032b036bab55bf1b7a2a00b64c.
No material blocker remains in the Java pointer implementation at this head. Complete-source inspection resolves all 11 non-APPROVE panel records. This is a source-based formal verdict with partial panel coverage, not a claim of a successful Java build.
Source and test coverage
- REST/gRPC pointer clients, PointerWire, model records, typed errors, proto messages, retained-payment hints and test source checked against the frozen #346 daemon. The parent independently checked counter parsing, optional finality counters, tagged status handling and the daemon-generated retention suffix.
- The parent verified that the Java tree's pointers.proto and common.proto are byte-identical to those in the frozen #346 daemon checkout, and verified each reference checkout's HEAD against its frozen inventory.
- Tests were inspected, not run. No Java runtime is installed in this environment; no JDK, Maven or Gradle download was attempted.
Findings and adjudication
- No reachable counter overflow/null failure with this daemon: PointerWire.java:36-66 requires ASCII decimal strings in the unsigned-64 range using BigInteger, with separate optionalCounter helpers. REST and gRPC finality use those optional helpers (:164-183,302-317); write/record/prepare/finalize counters are required daemon strings.
- The kind_tag narrowing claim is not reachable under the pinned contract: daemon types.rs:706/720/729 uses u8. Unknown kinds preserve the raw tag and degrade to the unknown-kind model rather than failing a conforming response.
- GrpcAntdClient.java:153-190 checks the x-antd-error-code tag against its matching status before assigning a pointer error. Its end-anchored retained-payment parser matches the exact suffix generated in daemon pointers.rs:714-718. No automatic new payment or paid-write retry is introduced.
- Signed-quote conversion is not double base64 encoding: the daemon converts its REST base64 strings to proto bytes (grpc/service.rs:146-165); Java converts those bytes back to the shared base64 representation. Client-side AntdException(0) is an existing SDK convention, not a new retry state.
- Residual compatibility risk, nonblocking: a future daemon that changes the retention-message suffix could suppress the parsed retryable hint. Keep a regression against the daemon format; this verdict is not compatibility assurance for arbitrary future servers.
Panel dissent and its disposition
- Four CHANGES_REQUIRED records and seven UNCERTAIN records were resolved against complete files. The purported missing models/wire/client implementation was present; fragment-scoped absence was not production absence.
- The strict-counter objection confused required counter fields with optional finality. The kind-tag objection used the proto's widened uint32 range instead of the daemon's actual u8 domain. Both blocking premises are refuted for this co-release contract.
- The suffix-anchor and base64 objections are refuted by the daemon's emitted message and conversion code. This adjudication is not an extra independent panel vote; original seat verdicts and unavailable seats remain listed below.
Review team: actual participation
18/24 scope/role/model seats returned substantive, receipt-validated reviews; 3/3 distinct named models. Partial panel; provisional coverage limits apply to this formal verdict. A receipt is evidence of participation, not proof that its findings are correct. Counts below are per evidence scope, not additional independent models.
source-1: glm: correctness=APPROVE, adversarial=UNCERTAIN; qwen: correctness=UNCERTAIN, adversarial=CHANGES_REQUIRED; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=unavailable (Incomplete termination; unavailable seat).source-2: glm: correctness=APPROVE, adversarial=APPROVE; qwen: correctness=CHANGES_REQUIRED, adversarial=CHANGES_REQUIRED; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=CHANGES_REQUIRED.source-3: glm: correctness=APPROVE, adversarial=unavailable (Incomplete termination; unavailable seat); qwen: correctness=APPROVE, adversarial=UNCERTAIN; ds4: correctness=unavailable (Incomplete termination; unavailable seat), adversarial=unavailable (Incomplete termination; unavailable seat).source-4: glm: correctness=UNCERTAIN, adversarial=APPROVE; qwen: correctness=UNCERTAIN, adversarial=UNCERTAIN; ds4: correctness=APPROVE, adversarial=UNCERTAIN.- Models: GLM 5.3 / OpenRouter z-ai/glm-5.3; local Qwen 3.8 / qwen38-27b; local DS4 Flash / deepseek-v4-flash. Missing seats received the configured same-model retry; no replacement model is counted as an independent vote.
Publication gates and limits
- Live head rechecked against the reviewed head. Live base
cea236950084f8738106cc37247c148f0b411d18: binary diff matches the reviewed diff, merge base is unchanged, no intersecting base-drift paths, and trial merge exited 0. - GitHub check snapshot: no checks reported; this is not a passing CI result.
- No Java compilation, unit tests, live tests or paid integration tests were executed. Source review cannot establish runtime or dependency compatibility.
- No paid or live-network tests. No merge, deployment, source change, or Linear state change. This verdict applies only to the exact head above; it is not a fleet-wide or cross-SDK integration sign-off.
Part of the pointer API fan-out. Stacked on #326; review that first.
AntdClient, AsyncAntdClient and GrpcAntdClient gain the daemon's
pointer surface, with the same models and exceptions on all three:
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:
pointer, and an async read
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.
Version bumps happen at release time; nothing here bumps a package version.
🤖 Generated with Claude Code