Skip to content

Discovery & Transport

Discovery Flow

Diagram

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 the INKAgentEndpoint service entry. A card whose ownerDid does not match the resolved DID MUST be rejected.
  • The card’s agentId is the agent’s own identifier (a tulpa:/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 whose agentId does not match the card’s agentId field.
  • When the DID resolution chain produced a delegated agentLink record, 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-Control MUST be honored. Consumers that cache cards MUST observe max-age and no-store from 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 keyId hint. An inbound message carries a keyId header that the cached card does not list.
  • keySetVersion increase observed. The card carries keySetVersion: N; any subsequent fetch returning keySetVersion > N MUST 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 protocol at the top level, either ink/0.1 or ink/0.2. This is the wire version, not the spec/package release version. ink/0.2 differs from ink/0.1 only in the body-signature domain separator (ink/sign\n instead of the legacy tulpa/sign\n); see Authentication. A receiver MUST verify the body signature under the domain selected by the message’s protocol and MUST reject an unknown version. Senders emit ink/0.1 by default and emit ink/0.2 only to a receiver that advertises support for it. A receiver advertises the versions it verifies in its Agent Card supportedProtocolVersions array; when that field is absent, assume ink/0.1 only.

Naming Convention: Lexicon IDs vs. Wire Types

INK uses two distinct identifier formats for message types. Implementations MUST distinguish between them:

PurposeFormatExample
AT Protocol Lexicon ID (schema registry)camelCase, includes ink namespace segmentnetwork.tulpa.ink.auditQuery
Wire type field (JSON message body)snake_case, omits ink segmentnetwork.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.