Discovery & Transport
Discovery Flow
Endpoint Discovery
The DID document is the sole authoritative source for endpoint discovery. Agents locate a target’s Agent Card by resolving the target DID and finding the INKAgentEndpoint service entry. The serviceEndpoint value is the URL of the Agent Card, not the inbound message endpoint:
{ "id": "#inkAgent", "type": "INKAgentEndpoint", "serviceEndpoint": "https://agent.example.com/ink/v1/alice/agent.json"}The Agent Card itself carries the inbound message URL in its endpoint field. A consumer fetches the card from serviceEndpoint, then POSTs signed envelopes to the card’s endpoint.
If no INKAgentEndpoint service entry exists in the DID document, the DID is not INK-reachable. The agentLink record provides supplementary key material and delegation proof but does NOT contain an endpoint field.
Legacy service entry name
For deployments published before this release, type: "TulpaAgentEndpoint" MUST also be accepted as a synonym for INKAgentEndpoint while the ink/0.x wire line is current. New publishers SHOULD emit INKAgentEndpoint; consumers MUST accept either. The vendor-named legacy synonym is slated for removal only at a future incompatible wire-version bump, not at ink/0.2 (which differs from ink/0.1 solely in the body-signature domain).
When a DID document contains both, INKAgentEndpoint takes precedence.
Agent Card identity binding (MUST)
The Agent Card fetched from serviceEndpoint carries two identifiers that consumers MUST bind to the resolution context:
- The card’s
ownerDid(when present) MUST equal the DID being resolved — i.e. the human DID whose DID Document contained theINKAgentEndpointservice entry. A card whoseownerDiddoes not match the resolved DID MUST be rejected. - The card’s
agentIdis the agent’s own identifier (atulpa:/did:/agent:shape depending on the platform). The consumer MUST bind it to whatever value it intends to send to (URL path param, address-book entry, etc.). For inbound deliveries the routing layer MUST refuse to publish a card under a path whoseagentIddoes not match the card’sagentIdfield. - When the DID resolution chain produced a delegated
agentLinkrecord, the card’s published key material MUST be consistent with the keys delegated by that record.
This is the structural defense against a host that legitimately publishes one DID claiming to publish another at the same URL: the ownerDid field binds the card to the resolved human, the agentId field binds it to a routable agent identifier, and either mismatch is a hard reject.
Discovery fetch security (MUST)
Discovery follows attacker-controllable URLs. The following requirements apply to every HTTP fetch a consumer issues while resolving a DID, fetching a DID Document, or fetching an Agent Card:
- HTTPS only. Plaintext
http://MUST be refused (TLS 1.2+). - No private or reserved hosts. The destination hostname MUST NOT resolve to a loopback (127.0.0.0/8, ::1), link-local (169.254.0.0/16, fe80::/10, cloud-metadata 169.254.169.254), unique-local (fc00::/7), private (10/8, 172.16/12, 192.168/16) or multicast/reserved range. IP-literal hosts SHOULD be rejected entirely.
- Bounded redirects. Consumers MUST cap redirect depth (recommended: 3) and MUST re-apply the host check on every redirect target. Cross-host redirects on
did:web:document fetches SHOULD be refused. - Size and time caps. Consumers MUST cap response body size (recommended: 64 KB for an Agent Card or DID Document) and request timeout (recommended: 5 seconds). Truncated or timed-out responses MUST be treated as a failed fetch, not a partial card.
Cache-ControlMUST be honored. Consumers that cache cards MUST observemax-ageandno-storefrom the response.
Detailed SSRF hardening (DNS pinning, egress allow-lists, runtime egress controls) is the implementer’s responsibility. The list above is the normative floor; any implementation that violates one of these requirements is not INK-conformant.
Agent Card cache and refresh (MUST)
A consumer MAY cache Agent Cards subject to the rules above. Beyond Cache-Control, a consumer MUST refetch a card when any of the following occur:
- Signature verification miss. An inbound message verifies against no key in the cached card.
- Unknown
keyIdhint. An inbound message carries akeyIdheader that the cached card does not list. keySetVersionincrease observed. The card carrieskeySetVersion: N; any subsequent fetch returningkeySetVersion > NMUST replace the cached card immediately.
A consumer MUST NOT fall back to a bootstrap key (one derived from the DID itself) after it has observed a valid published key set for that DID, even if every key in the cached card has since rotated to revoked. Bootstrap keys are only valid before any card has been observed.
Transport
- Protocol: HTTPS / REST. All INK endpoints MUST be served over TLS 1.2+.
- Content-Type:
application/json. - Protocol version on the wire: every INK message MUST include
protocolat the top level, eitherink/0.1orink/0.2. This is the wire version, not the spec/package release version.ink/0.2differs fromink/0.1only in the body-signature domain separator (ink/sign\ninstead of the legacytulpa/sign\n); see Authentication. A receiver MUST verify the body signature under the domain selected by the message’sprotocoland MUST reject an unknown version. Senders emitink/0.1by default and emitink/0.2only to a receiver that advertises support for it. A receiver advertises the versions it verifies in its Agent CardsupportedProtocolVersionsarray; when that field is absent, assumeink/0.1only.
Naming Convention: Lexicon IDs vs. Wire Types
INK uses two distinct identifier formats for message types. Implementations MUST distinguish between them:
| Purpose | Format | Example |
|---|---|---|
| AT Protocol Lexicon ID (schema registry) | camelCase, includes ink namespace segment | network.tulpa.ink.auditQuery |
Wire type field (JSON message body) | snake_case, omits ink segment | network.tulpa.audit_query |
The lexicon ID follows AT Protocol conventions (camelCase NSID with hierarchical namespacing). The wire type is the value in the "type" field of every INK message, it is what implementations match on when routing messages.
Implementations MUST key off the wire type field, not the lexicon ID.
For single-word types the distinction is invisible (network.tulpa.intent maps to lexicon network.tulpa.ink.intent). For multi-word types it is significant: network.tulpa.audit_query (wire) vs. network.tulpa.ink.auditQuery (lexicon).
The network.tulpa.* wire namespace is frozen for v0.x per the INK compatibility policy. Renaming the wire discriminator would break every deployed router; the vendor-leaked name is paid debt that will be settled only at a future incompatible wire-version bump. The ink/0.2 bump does not touch it, since ink/0.2 differs from ink/0.1 solely in the body-signature domain.
See Wire Types Reference for the complete mapping.