UAI Seeker SDK: Low-Level Design (LLD)

How the Seeker SDK is built — discovering Providers, obtaining delegation tokens, and interacting peer-to-peer.

Scope. This is the how of the Seeker SDK. The what (lifecycle ownership, trust model, platform shape) lives in the platform HLD. Where the HLD says a Seeker exists, this page says how the library is built. The UAI Technical Specifications and frozen JSON Schemas remain the normative contract; this LLD implements them and must not contradict them.

Status. First draft for iteration. Library and shape choices marked (proposal) are open to change.


Index

  1. Purpose and scope
  2. Responsibilities
  3. Domain model
  4. Runtime state
  5. Core interfaces
  6. API surface
  7. Key flows
  8. Concurrency model
  9. Validation
  10. Security summary
  11. Observability
  12. Configuration
  13. Error model
  14. Testing and conformance
  15. Phase 2 hooks (deferred)
  16. Open items to confirm
  17. Versioning

1. Purpose and scope

A Seeker is any application that initiates a UAI interaction: discover a Provider, obtain a delegation token, call the Provider peer-to-peer over TLS 1.3, and verify that a hash-only receipt was recorded in the Audit Ledger.

The Seeker SDK (seeker/) is a thin Go library built entirely on uai-core/pkg/*. An adopter supplies configuration and domain logic; the SDK owns discovery, token requests, envelope construction and signing, P2P transport (the signed {context, message, proof} envelope on the wire over TLS 1.3), Bearer presentation (RFC 6750), and receipt verification.

This document covers the SDK’s module layout, domain model, public API, core flows, envelope profile, concurrency, validation, security, observability, errors, and testing. It does not cover the Registry, Trust Server, Audit Ledger, or Provider Kit implementations except where the Seeker calls them.

Phase 1 delivers the SDK (sub-epic 4A) and the first adopter — NAFPO Seeker (sub-epic 4B) — on top of a stable control plane (Epic #2).

2. Responsibilities

The Seeker SDK does:

  • Optionally register the Seeker in the Registry (challenge → proof-of-control → POST /registry/v1/agents, role: seeker).
  • Discover Providers via GET /registry/v1/discover, rank results, and select a Provider for Query().
  • Request delegation tokens via POST /trust/v1/token (Tier 1–2 in Phase 1), first proving control of the Seeker DID with the Trust Server’s GET /trust/v1/challenge proof-of-control (Trust Server LLD §7.1).
  • Bind the one transaction_id minted by the Trust Server (returned in TokenResponse) across envelope context, JWT txn claim, and receipt. The SDK never generates its own transaction_id; minting server-side keeps the namespace under Trust Server control.
  • Build the {context, message} envelope from the adopter’s flat payload, sign it (envelope proof), and send the {context, message, proof} envelope on the P2P wire with Authorization: Bearer (RFC 6750).
  • Verify the Provider’s response envelope proof, and canonicalize {context, message} (proof excluded) for receipt digest alignment with the Provider Kit.
  • Poll the Audit Ledger and verify request_hash, response_hash, Provider EdDSA signature, and participant DIDs.
  • Expose a small, stable public API (New, Discover, Token, Query, VerifyReceipt, ListReceipts, Register).

The Seeker SDK does not:

  • Reimplement canonicalization, envelope hashing, JWT verification, or control-plane HTTP (all from pkg/*).
  • Store or forward Provider payloads through UAI control-plane services.
  • Issue delegation tokens or evaluate risk policy (Trust Server).
  • Fetch or validate consent artifacts (adopter passes consent_id; Trust Server validates).
  • Generate Tier 3 DPoP proofs in Phase 1 (seam only; see §17).
  • Perform NANDA federation or semantic discovery ranking in Phase 1.

Module layout

One importable library, compiler-walled internals, shared primitives imported from pkg/. Domain types and port interfaces live in internal/model/; each outbound integration (Registry, Trust, Audit, P2P) is a separate package behind those interfaces.

seeker/
  seeker.go                 public API surface
  config.go                 Config validation
  options.go                functional options (timeouts, poll policy)
  tokenopts/
    dpop.go                 Phase 2 no-op stub
  internal/
    model/                  domain types + sentinel errors
                            + port interfaces: Discoverer, TokenMinter, EnvelopeBuilder,
                              Transporter, ReceiptVerifier, Registrar
    query/                  Query() orchestration
    discover/               Registry discover + ranking
    token/                  Trust Server token shaping
    envelope/               {context, message} construction, sign/verify proof + canonicalization (uses pkg/envelope, pkg/jcs, pkg/ed25519x)
    transport/              P2P POST /uai/v1/query — {context, message, proof} envelope body, Bearer, TLS 1.3
    receipt/                ledger poll + verify (uses pkg/receipt)
    register/               optional Registry self-registration
cmd/seeker-template/        runnable minimal Seeker against toy Provider
conformance/seeker/         operational definition of correct Seeker behaviour (CI)
pkg/                        shared, byte-identical across services and kits
  jcs/                      RFC 8785 canonicalization
  envelope/                 Envelope type + CanonicalBytes()
  receipt/                  receipt.Verify()
  transport/                TLS 1.3 HTTP client
  did/                      did:webvh resolver (web + verifiable history)
  ed25519x/                 Ed25519 sign/verify (Register() proof-of-control + envelope proof)
  schema/                   frozen JSON Schemas + loader (response validation)
  clients/                  registry, trust, audit REST clients (generated from OpenAPI)

Boundary rule. Nothing in seeker/internal/... may reimplement logic that already lives in pkg/. Cross-service HTTP goes only through pkg/clients/*. This is a depguard rule in CI, so a violation fails the build rather than relying on review.

Dependency direction. internal/model/ defines domain types, sentinel errors, and port interfaces, and imports nothing else internal. Every other internal package depends on model/, never the reverse. The public seeker package wires concrete implementations in New() and exposes the stable surface. Keeping interfaces at the boundary means unit tests use fakes (no network), and swapping a client implementation does not touch orchestration code.

Sibling: Provider Kit. The Seeker SDK and Provider Kit (Epic #3) share pkg/jcs, pkg/envelope, and conformance vectors (#71). Both must implement the same Phase 1 envelope profile (§8.1) so receipt digests match byte-for-byte.

Why interfaces at the boundaries. Because Discoverer, TokenMinter, EnvelopeBuilder, Transporter, ReceiptVerifier, and Registrar are interfaces, orchestration stays testable with fakes, outbound adapters are swappable, and a contributor has a small, clear surface to satisfy.

One contract, generated clients. Control-plane REST surfaces are described in each service’s openapi.yaml. From them, oapi-codegen (proposal) generates pkg/clients types the SDK uses. Client and server cannot drift; CI fails if generated code is stale.

5. Domain model

The adopter supplies Config and a flat domain payload. The SDK never exposes raw HTTP to adopters.

Configuration (Config)

FieldRequiredNotes
SeekerDIDyesThis agent’s did:webvh:...
RegistryURLyesControl plane Registry base URL
TrustURLyesTrust Server base URL
AuditURLyesAudit Ledger base URL
IdentityyesEd25519 signer for Register() proof-of-control and P2P envelope proof (pkg/ed25519x)
HTTPClientnoTest override; default uses pkg/transport (TLS 1.3)
AuditAuthnoMints Seeker-scoped JWT for ledger reads
DefaultCountrynoDefault envelope context.country (e.g. IND)
PollTimeoutnoMax wait for receipt after P2P (default aligned with P95 E2E < 2s)

Request and response types

TypeFields (summary)
DiscoverQueryscope, optional capability, tier_max, data_residency, cursor, page_size
ProviderRecorddid, endpoints[], supported_scopes[], risk_tier_max, india_compliance.data_residency, facts_ref
TokenRequestprovider_did, scope, optional consent_id (Tier 2+)
TokenResponseaccess_token, transaction_id (minted by the Trust Server), tier, expires_in
QueryRequestscope, optional provider_did, payload (flat domain JSON), optional consent_id
QueryResponsedata, transaction_id, receipt (VerifiedReceipt)
VerifiedReceiptLedger receipt + status: verified | unverifiable
Envelope{context, message, proof} — serialized as the P2P HTTP body; proof signs the canonical {context, message}

Critical invariant

The {context, message} portion of the envelope the SDK sends on the wire must canonicalize identically to what the Provider hashes when signing its receipt. The envelope proof signs that same canonical form and is itself excluded from the hash (spec §3.4), so the wire body and the hashed form never diverge. The SDK builds and signs the envelope from the adopter’s flat payload via pkg/jcs + pkg/ed25519x; adopters never construct or sign envelopes themselves.

6. Runtime state

The Seeker SDK is stateless between calls. It holds no database and persists no Provider payloads. During a single Query() it keeps in memory only:

  • the request and response {context, message, proof} envelopes,
  • the delegation JWT (never logged),
  • poll state for the receipt fetch.

This matches the notary model: payloads flow P2P only; the SDK and control plane never store them. Receipts in the Audit Ledger store hashes only (spec §5.2).

7. Core interfaces

Orchestration depends on these ports, not on raw HTTP clients.

type Discoverer interface {
    Discover(ctx context.Context, q DiscoverQuery) ([]ProviderRecord, error)
    Select(providers []ProviderRecord, scope string) (*ProviderRecord, error)
}

type TokenMinter interface {
    Mint(ctx context.Context, req TokenRequest) (TokenResponse, error)
}

type EnvelopeBuilder interface {
    // BuildRequest wraps the flat payload into {context, message} and signs it (proof).
    BuildRequest(meta ContextMeta, payload map[string]any) (*envelope.Envelope, error)
    // ParseResponse parses the {context, message, proof} response and verifies proof.
    ParseResponse(responseBody []byte) (*envelope.Envelope, error)
}

type Transporter interface {
    Query(ctx context.Context, endpoint string, token string, body []byte) ([]byte, error)
}

type ReceiptVerifier interface {
    WaitAndVerify(ctx context.Context, txnID string, req, resp *envelope.Envelope) (VerifiedReceipt, error)
    List(ctx context.Context, filter ReceiptFilter) ([]VerifiedReceipt, error)
}

type Registrar interface {
    Register(ctx context.Context) error
}

Phase 1 implementations delegate to pkg/clients/registry, pkg/clients/trust, pkg/clients/audit, pkg/envelope, pkg/transport, and pkg/receipt.

8. API surface

Public Go API — stable product surface; integration guide and template are written against exactly this.

MethodLifecycle stepPurpose
New(Config)Construct SDK; validate config
Register()1Optional Registry self-registration
Discover()2Find Providers by scope / tier / residency
Token()3Obtain delegation JWT
Query()2→4→6Full orchestration
VerifyReceipt()6Fetch and cross-check one receipt
ListReceipts()7List receipts visible to this seeker

Minimal adopter call:

s, err := seeker.New(cfg)
resp, err := s.Query(ctx, seeker.QueryRequest{
    Scope:   "read:fpo_data",
    Payload: map[string]any{"state": "Maharashtra", "district": "Pune"},
})

Lower-level methods exist for multi-provider chains; adopters must not reimplement protocol HTTP.

8.1 Phase 1 envelope profile

The spec defines a full {context, message, proof} envelope (§3.3) and requires one signing mechanism for v1 conformance (§3.3). Phase 1 sends the complete signed envelope on the wire: the SDK signs the canonical {context, message} and carries the signature in proof.

LayerPhase 1 behaviour
On the wire (P2P HTTP body)Full {context, message, proof} envelope JSON
Envelope proofDetached JWS (EdDSA) over canonical {context, message}; verification_method = <seeker_did>#<kid>, signed with Config.Identity (pkg/ed25519x)
For hashing / receipt verifyCanonicalize {context, message} via pkg/jcs; proof excluded from the hash (spec §3.4)
Provider agreementProvider verifies the request proof and signs its response envelope proof; digests match byte-for-byte
Audit Ledgerrequest_hash / response_hash only; no envelope stored (spec §5.2)

The proof signs the same canonical {context, message} that feeds the receipt hash, and proof is excluded from that hash (spec §3.4). So the wire body and the hashed form never diverge, while the signature adds end-to-end sender authentication, body integrity, and non-repudiation that survive TLS termination. The adopter still supplies only a flat domain payload; the SDK wraps and signs it (adopters never build or sign envelopes by hand).

Signing mechanism (v1). UAI v1 requires a single request-integrity mechanism (spec §3.3, recommending RFC 9421 HTTP Message Signatures or JWS in proof). Phase 1 selects EdDSA JWS in the envelope proof, reusing the Ed25519 DID key already used for registration proof-of-control (Registry LLD §8.1) and receipt signatures, so one key type spans the system. Confirm this choice against the Provider Kit and shared vectors (§18).

Request authenticity (Phase 1). Three layers: envelope proof (application-layer signature), Bearer delegation JWT (RFC 6750), and TLS 1.3. The Provider validates the JWT offline (ZTAA, spec §8.1) and verifies the envelope proof by resolving the Seeker’s did:webvh and checking the signature over canonical {context, message}. Tier 3+ additionally binds the token with a DPoP HTTP header when cnf.jkt is present (Phase 2 seam, §17).

Context fields the SDK populates: domain, country, version, action, seeker_did, provider_did, transaction_id, message_id, timestamp, ttl; Tier 2 adds citizen_context.consent_id.

Canonicalization. Envelope.CanonicalBytes() returns JCS bytes of {context, message} with proof excluded (spec §3.4). Hash label in receipt: jcs:rfc8785.

8.2 Discovery filters and ranking

Discover() calls GET /registry/v1/discover with filters (all optional, AND-combined; contract per Registry LLD §8.2):

ParamPurpose
scopeProvider must support this scope
capabilitySubtree match over the Registry capability taxonomy (Registry LLD §9.6)
tier_maxCeiling on risk_tier_max (risk_tier_max <= tier_max)
data_residencyHard filter (e.g. IN)
cursor / page_sizeOpaque cursor pagination; page_size default 20, max 100

The Registry response envelope is {items, next_cursor, page_size}; Discover() follows next_cursor transparently when the caller requests more than one page.

Capability grounding. When the adopter supplies a capability, the SDK expects a canonical taxonomy id. It may first fetch GET /registry/v1/capabilities (cacheable, version-stamped) to ground a supplied label/alias onto a canonical id, rather than free-forming a urn.

Zero results. A zero-result discover carries the Registry’s broaden_to hint (the parent capability id). The SDK surfaces this via ErrNoProviders; it never silently broadens a hard filter to manufacture a match.

Ranking (kit-side, deterministic, applied after the Registry returns candidates):

  1. Prefer data_residency = IN when configured.
  2. Prefer lower risk_tier_max.
  3. Prefer records with facts_ref present.

Query() selects the highest-scoring Provider unless QueryRequest.ProviderDID is set. Hard filters are never relaxed to manufacture a result. Ranking is a client-side convenience only; the Registry match itself stays authoritative and unranked in Phase 1 (Registry LLD §9.6).

8.3 P2P transport

Every Provider call:

POST {endpoints[].uri}    // typically .../uai/v1/query
Content-Type: application/json
Authorization: Bearer <delegation-jwt>

<body> = signed {context, message, proof} envelope, built from adopter payload
  • TLS 1.3 only; no 1.2 fallback.
  • Resolve endpoint from the selected Provider’s Registry record.
  • Response body is a {context, message, proof} envelope; the SDK verifies the Provider proof, then canonicalizes {context, message} for receipt verification.

Endpoint safety (SSRF defense). The P2P target URL comes from a Registry record — data controlled by whoever registered the Provider. A malicious Provider could register http://169.254.169.254/... or another internal address and turn every adopter’s Seeker into an SSRF proxy inside their network. The Registry LLD already hardens its own resolver; the SDK mirrors those controls on the client side before and during every P2P call, and rejects an unsafe endpoint with ErrUnsafeEndpoint before dialing:

  • Require https. Reject any non-https scheme; combined with the TLS 1.3-only rule there is no plaintext or downgrade path.
  • Block internal / non-routable targets. Resolve the host and reject loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, cloud metadata), and RFC 1918 / unique-local private ranges. Re-check after DNS resolution to defeat DNS-rebinding.
  • Disable redirects. The HTTP client does not follow 3xx; a redirect to an internal address is treated as a failure, not chased.
  • Cap response size. Read the response body through a hard byte limit (io.LimitReader) so a hostile or buggy Provider cannot exhaust memory.
  • Bounded timeouts. Explicit connect, TLS-handshake, read, and total-request timeouts (aligned with the PollTimeout budget), all under context.Context cancellation.

Response envelope binding checks. Verifying the Provider’s proof signature is necessary but not sufficient — a valid signature only proves the bytes were signed by that key, not that they belong to this interaction. Before accepting a response the SDK enforces all of the following, and fails with ErrResponseBindingMismatch if any check fails:

  1. Transaction binding. Response context.transaction_id must equal the request’s transaction_id. Without this check a Provider can replay an old, correctly signed response.
  2. Provider identity. Response context.provider_did must equal the Provider the SDK selected and called.
  3. Key origin. The proof.verification_method must resolve under that same provider_did (did:webvh document), so the signature comes from a key the Provider actually controls.
  4. Freshness. context.timestamp and ttl must pass a defined clock-skew window (bounded allowed skew; reject stale or future-dated responses), closing the replay window that a matching txn alone does not.

These checks run after signature verification and before the response is handed to receipt verification or returned to the adopter.

8.4 Retry semantics

Retries are decided per endpoint by idempotency, not applied blindly. The SDK only auto-retries operations that are safe to repeat; everything else is surfaced to the caller.

EndpointMethodSafe to retry?Rule
GET /registry/v1/discoverGETYes (idempotent)Idempotent read. Retry with exponential backoff + jitter on 5xx, 429 (honour Retry-After), and transport errors. No retry on 4xx (except 429).
GET /registry/v1/capabilitiesGETYes (idempotent)Idempotent, cacheable read; same backoff as discover.
GET /registry/v1/challengeGETYes (idempotent)Idempotent; each call yields a fresh single-use nonce.
POST /trust/v1/tokenPOSTOnly with a fresh challengeRetryable only after a new GET /trust/v1/challenge — a consumed challenge/nonce cannot be reused. On a retryable failure, re-fetch the challenge, re-sign, then re-POST. Never replay the same signed request.
POST /uai/v1/query (P2P)POSTNo (not idempotent)Not idempotent — the Provider may have already processed the request and submitted a receipt. Do not blind-retry (risks duplicate side effects and a second receipt). Surface ErrP2PFailed; the adopter decides whether a new Query() (new transaction_id) is appropriate.
GET /audit/v1/receiptsGETYes (idempotent)Idempotent read; this is the poll loop itself (exponential backoff + jitter until receipt or PollTimeout).
POST /registry/v1/agents (Register)POSTOnly with a fresh challengeRetryable only after a new challenge, like token. Registration is effectively idempotent server-side (same DID), but each attempt needs an unconsumed nonce.

Retry budgets are bounded (max attempts + total deadline) and always respect context.Context cancellation and Retry-After. A 429 or 503 with Retry-After uses that hint instead of the computed backoff.

9. Key flows

9.1 Query() orchestration

Ordered so cheap local checks happen before network calls.

  1. Validate QueryRequest (scope, payload, consent rules).
  2. Discover (skip if ProviderDID set): Registry discover + rank + select.
  3. Token: prove control of the Seeker DID, then mint a token. Fetch a one-time nonce from GET /trust/v1/challenge?did=<seeker_did>, sign the JCS-canonical bound object (did, htm, htu, kid, nonce, body_sha256) via Config.Identity, and POST /trust/v1/token carrying the proof in the X-UAI-Nonce and X-UAI-Signature headers (same proof-of-control as registration, Trust Server LLD §7.1). Obtain access_token and transaction_id.
  4. Build & sign request envelope: wrap payload into {context, message}, sign the canonical form to produce proof; serialize {context, message, proof} for the wire and keep the canonical {context, message} bytes for the digest.
  5. P2P: POST /uai/v1/query with the signed {context, message, proof} envelope body + Bearer over TLS 1.3.
  6. Parse & verify response envelope: read the {context, message, proof} response, verify the Provider proof, then run the response binding checks (§8.3) — context.transaction_id equals the request’s, context.provider_did equals the selected Provider, proof.verification_method resolves under that did:webvh, and timestamp/ttl pass the clock-skew window — before canonicalizing {context, message} for verification.
  7. Poll ledger: GET /audit/v1/receipts?transaction_id=... with Seeker-scoped auth; exponential backoff until receipt or PollTimeout.
  8. Verify: recompute hashes; pkg/receipt.Verify for Provider signature and participants.
  9. Return QueryResponse. Hash mismatch → unverifiable (spec §3.4), surfaced clearly.
sequenceDiagram
    autonumber
    participant App as Adopter
    participant SDK as Seeker SDK
    participant Reg as Registry
    participant TS as Trust Server
    participant Prv as Provider
    participant Aud as Audit Ledger

    App->>SDK: Query(scope, payload, consentID?)
    SDK->>Reg: GET /registry/v1/discover
    Reg-->>SDK: Provider records
    SDK->>TS: GET /trust/v1/challenge?did=seeker_did
    TS-->>SDK: one-time nonce
    SDK->>SDK: sign bound object (proof-of-control)
    SDK->>TS: POST /trust/v1/token<br/>X-UAI-Nonce · X-UAI-Signature
    TS-->>SDK: access_token, transaction_id
    SDK->>SDK: build + sign {context, message, proof}
    SDK->>Prv: POST /uai/v1/query<br/>{context, message, proof} · Bearer JWT
    Prv-->>SDK: {context, message, proof} response
    SDK->>SDK: verify response proof + binding<br/>(txn, provider_did, vm, timestamp/ttl)
    Note over Prv,Aud: Provider submits receipt async
    SDK->>Aud: GET /audit/v1/receipts?transaction_id=...
    Aud-->>SDK: UAIReceipt
    SDK->>SDK: verify hashes + Provider signature
    SDK-->>App: QueryResponse{data, receipt}

9.2 Optional Seeker registration

Same proof-of-control model as the Registry LLD §8.1:

  1. GET /registry/v1/challenge?did=<seeker_did> → one-time nonce (5-min TTL).
  2. Sign the RFC 8785 (JCS) canonical form of the bound object (did, htm, htu, kid, nonce, body_sha256) with the Seeker’s Ed25519 key (pkg/ed25519x).
  3. POST /registry/v1/agents with role: seeker, carrying the proof in the X-UAI-Nonce and X-UAI-Signature headers (Registry LLD §8.1).

Tier 2+ Seekers must declare consent_endpoint per Registry schema. Steel-thread demos may use a pre-registered DID.

9.3 Receipt verification semantics

If the Seeker recomputes a hash that does not match the receipt, the SDK returns unverifiable, not a hard P2P failure (spec §3.4). The interaction may have succeeded; the audit trail cannot be cryptographically confirmed by this Seeker. Provider signature failure is also unverifiable.

Participant check: seeker_did and provider_did in the receipt must match the interaction.

10. Concurrency model

The SDK is I/O-bound. Query() runs sequentially: discover → token → P2P → poll → verify. We do not add goroutines to “speed up” a single happy-path call.

Concurrency-safe client. A seeker.Client (constructed by New()) is safe for concurrent Query() calls. It holds no per-call mutable state (§6) and reuses a single pkg/transport http.Client with keep-alives and per-host connection pooling across all goroutines. Adopters create one seeker.Client at startup and share it across every request handler: if ten callers hit the adopter’s server at once, ten Query() calls run on the same client concurrently, and that must be safe. Per-call state (envelopes, delegation JWT, poll state) lives only on the goroutine’s stack for the duration of that Query(), so calls never contend over shared fields.

Concurrency appears only where it removes a real wait:

  • Context cancellation: every outbound call respects context.Context.
  • Poll backoff: one goroutine per Query() polls the ledger with exponential backoff and jitter under PollTimeout.
  • Multi-provider chains: adopter orchestration (multiple Token + P2P calls); the SDK does not ship a chain DSL in Phase 1.

11. Validation

Three layers, in order:

  1. Config (structural). New() validates required URLs, Seeker DID format, identity key. Failure before any network I/O.
  2. Request shaping (cross-field). Token() requires consent_id for Tier 2 scopes. Query() rejects empty scope when required.
  3. External (runtime). Typed clients + pkg/schema validate responses. Response envelope proof is verified against the Provider’s did:webvh key → ErrEnvelopeProofInvalid on failure. Response binding checks (§8.3) — matching transaction_id, provider_did, verification_method origin, and timestamp/ttl skew — → ErrResponseBindingMismatch on failure. Receipt hash mismatch → ErrReceiptUnverifiable.

12. Security summary

  • P2P auth (Phase 1): Authorization: Bearer on every Provider call (RFC 6750). Provider validates JWT offline (ZTAA).
  • Transport: TLS 1.3 only (pkg/transport).
  • Envelope proof (Phase 1): every P2P request carries a detached JWS (EdDSA) proof over canonical {context, message}, signed with the Seeker’s did:webvh key (pkg/ed25519x); the Provider verifies it, and the SDK verifies the Provider’s response proof. This satisfies the spec §3.3 v1 signing requirement and adds end-to-end integrity and non-repudiation beyond JWT + TLS.
  • Envelopes: signed {context, message, proof} on the wire; {context, message} canonicalized for hashing; proof excluded from canonicalization (spec §3.4).
  • Response binding (anti-replay): a valid proof signature is not enough. The SDK also requires the response context.transaction_id to equal the request’s, context.provider_did to equal the selected Provider, proof.verification_method to resolve under that same did:webvh, and timestamp/ttl to pass a bounded clock-skew window (§8.3). This blocks replay of an old, correctly signed Provider response.
  • Receipt verification: independent hash recompute + Provider EdDSA signature; participant DIDs must match.
  • Registration: Registry proof-of-control per Registry LLD §8.1 when Register() is used.
  • SSRF defense (P2P): the Provider endpoint URL comes from a Registry record; the SDK requires https, blocks loopback/link-local/metadata/RFC 1918 targets (re-checked after DNS resolution), disables redirects, caps the response body size, and sets bounded timeouts (§8.3). Unsafe endpoints fail with ErrUnsafeEndpoint before dialing.
  • Bounded response reads: every remote body — Provider P2P responses and Audit Ledger reads — is read through a hard size cap (io.LimitReader), so a hostile or buggy peer cannot exhaust Seeker memory.
  • Unknown critical fields: after the proof check, the SDK rejects a response that carries any envelope field marked critical which it does not recognize, rather than silently ignoring it (fail-closed).
  • Error hygiene: error messages and logs never echo request/response payloads; they carry only transaction_id, DIDs, scope, and the verification outcome.
  • Logging hygiene: never log delegation JWTs or domain payloads; log transaction_id, DIDs, scope, verification outcome.
  • Audit read auth: Seeker-scoped JWT (sub = seeker DID); dev HS256 pattern in template; production profile TBD (§18).

13. Observability

  • Logging: structured log/slog; one summary line per Query() with transaction_id, provider_did, scope, latency, receipt status.
  • Metrics (optional Phase 1): discover/token/P2P/verify outcome counters; E2E Query() duration histogram.
  • Tracing (optional Phase 1): OpenTelemetry spans on discover, token, P2P, ledger poll.

The SDK does not submit receipts; it only verifies them.

14. Configuration

Typed Config struct; environment loading lives in cmd/seeker-template/, not in the library.

Field / optionMeaning
SeekerDIDThis agent’s DID
RegistryURL / TrustURL / AuditURLControl plane base URLs
IdentityEd25519 key for Register() and P2P envelope proof
PollTimeoutMax wait for receipt (default ~2s budget)
PollInitialIntervalFirst backoff interval
DefaultCountryEnvelope context.country default
HTTPClientTest override
AuditAuthToken minting for ledger reads

15. Error model

One sentinel-error set in internal/model/; public API wraps with %w for errors.Is.

ErrorWhen
ErrConfigInvalidMissing or malformed Config
ErrNoProvidersDiscover returned no matches
ErrConsentRequiredTier 2 scope without consent_id
ErrTokenDeniedTrust Server rejected token request
ErrP2PFailedProvider HTTP or TLS failure
ErrUnsafeEndpointProvider endpoint failed SSRF checks: non-https, internal/non-routable address, or redirect (§8.3)
ErrEnvelopeProofInvalidResponse envelope proof failed signature verification
ErrResponseBindingMismatchResponse transaction_id/provider_did/verification_method mismatch or failed timestamp/ttl skew check (§8.3)
ErrCriticalFieldUnknownResponse carries an unrecognized envelope field marked critical (§12, fail-closed)
ErrReceiptTimeoutLedger poll exceeded PollTimeout
ErrReceiptUnverifiableHash mismatch or bad Provider signature
ErrReceiptNotFoundNo receipt after poll

16. Testing and conformance

  • Unit: orchestration and ranking against fakes of §7 interfaces; no network.
  • Integration: local control plane + toy Provider; full discover → token → P2P → verify.
  • Conformance: conformance/seeker/ runs in CI on changes to seeker/ or dependent pkg/.
  • Shared vectors: envelope hashes from #71; must match Provider Kit byte-for-byte.
  • did:webvh conformance: pkg/did carries its own conformance vectors for did:webvh resolution and verifiable-history validation, wired into CI. Use the official did:webvh test suite/vectors where one exists rather than hand-rolling cases.
  • Wire assertion: P2P body is the signed {context, message, proof} envelope; the proof verifies over canonical {context, message}.
  • Security checks: deliberate hash regression, missing Bearer header, tampered-body / invalid-proof, SSRF endpoint rejection, and replayed / cross-txn responses all fail CI.
  • Contributor flow: go test ./seeker/...; go test ./conformance/seeker/...; CI runs both plus depguard.
Test areaAsserts
DiscoveryScope/tier/residency filters; ranking
Token Tier 1Token without consent_id
Token Tier 2Fails without consent; succeeds with valid test consent_id
BearerAuthorization: Bearer on P2P per RFC 6750
Wire bodySigned {context, message, proof} envelope on P2P
Envelope proofRequest carries valid EdDSA JWS proof; tampered body or bad proof rejected
Response bindingRejects a signed response with a mismatched transaction_id, wrong provider_did, foreign verification_method, or stale/future timestamp/ttl (replay defense)
SSRF defenseRejects non-https, loopback/link-local/RFC 1918, and redirecting Provider endpoints (ErrUnsafeEndpoint); enforces response size cap
did:webvhResolution + verifiable-history vectors (official suite where available)
Envelope hashesCanonical {context, message} digest matches #71
Receipt verifyVerified / Unverifiable distinction
E2Eseeker-template + Provider template full lifecycle

17. Phase 2 hooks (deferred)

  • Tier 3 DPoP: tokenopts/dpop.go; attach DPoP proof header on token + P2P when cnf.jkt present.
  • RFC 9421 HTTP Message Signatures: optional second sanctioned request-integrity mechanism alongside the Phase 1 JWS proof (crypto-agility; post-quantum suite migration via the DID verification method).
  • NANDA federation: discover via facts_ref; Registry epic #12.
  • Live Aadhaar OTP consent: adopter obtains consent; mock consent for Tier 2 demo in Phase 1.
  • Write / transactional scopes: confirm, track, cancel.
  • Semantic discovery ranking: non-authoritative suggestion in front of deterministic Registry match.
  • Receipt push / webhook: poll remains default; optional notify seam.
  • Multi-hop chains: Bhashini → Provider; adopter orchestration with SDK primitives.

18. Open items to confirm

  1. message nesting. Flat payload as message directly vs message.intent — must match Provider Kit and #71 vectors.
  2. Audit Ledger Seeker auth in Phase 1. Dev HS256 in template; production JWT profile aligned with Audit LLD.
  3. Query() poll timeout vs push. Poll with backoff in Phase 1; webhook seam later.
  4. Default Provider selection. Highest registry score; explicit ProviderDID override always wins.
  5. Wrap-rule alignment. Seeker and Provider Kit must both hash the wrapped {context, message} envelope (not the flat body) using identical wrap rules, or E2E receipt verification will not match. Locked by the shared vectors in #71.
  6. Envelope proof mechanism (#64). Phase 1 signs proof as a detached EdDSA JWS over canonical {context, message} (spec §3.3 alternative). Confirm this versus RFC 9421 HTTP Message Signatures as the v1 baseline, and align the exact proof shape (type, verification_method) with the Provider Kit and shared vectors (#71).
  7. Response freshness window. Fix the allowed clock-skew bound for the response timestamp/ttl binding check (§8.3) and align it with the Provider Kit and receipt ttl semantics.
  8. did:webvh test vectors. Adopt the official did:webvh conformance suite/vectors for pkg/did if one is published; otherwise define and maintain in-repo vectors (§16).

19. Versioning

The Seeker SDK is a library other teams import, so version discipline is part of its contract.

  • Module version (SemVer). The Go module follows Semantic Versioning: MAJOR.MINOR.PATCH. PATCH is bug fixes only, MINOR adds backward-compatible API, and MAJOR signals a breaking change. Per Go module rules, a v2+ release lives under a /v2 import path so adopters upgrade deliberately.
  • Public API stability. The stable surface is exactly the §8 API (New, Register, Discover, Token, Query, VerifyReceipt, ListReceipts) plus the exported types they use. Anything under seeker/internal/... is not covered by these guarantees and may change in any release.
  • Protocol version. The UAI protocol version the SDK speaks is carried in envelope context.version (§8.1) and is independent of the SDK’s own module version; one SDK line targets one protocol major version. A protocol-breaking change is a MAJOR SDK bump.
  • Spec and schema pinning. The SDK pins the frozen JSON Schemas and shared vectors (#71) it was built against; a schema/vector change that alters behaviour is released as a new SDK version rather than silently.
  • Deprecation policy. A method or field is marked deprecated (Go // Deprecated: doc comment) for at least one MINOR release before removal in the next MAJOR, with the replacement named in the deprecation note.
  • Reporting. The SDK exposes its version (e.g. seeker.Version) for logs and support, and includes it in the User-Agent on outbound control-plane calls.