Attaching an identity credential — a credential about who the sender is, not about what it may do.
An identity credential rides in the same attachments list, with the same
media_type, as a Verifiable Grant. The cloud-node tells the two apart on one
test — credentialSubject.scope: present and non-empty is a grant and feeds
the policy's credentials input; absent or empty is an identity credential
and feeds sender_credentials, where a grant's
constraints.senderCredentials requirement can see it.
Why this is a separate path, not a wallet feature
Layr8.Wallet SELECTS grants: it ranks candidates by how well their scope
covers the outbound message. An identity credential has no scope, so it cannot
be ranked — and there is nothing here for the wallet to select BY either. The
requirement it would have to satisfy lives in the grant held by the RECIPIENT
and never reaches the sender before the call.
So an SDK that chose identity credentials automatically would have exactly one implementable behaviour: attach everything the holder has. That is a disclosure decision wearing the costume of a convenience feature. Which claims about a person or an organisation a counterparty gets to see is the holder's call, made per message.
The caller names the credential. This module only builds the envelope.
{:ok, creds} = Layr8.Client.list_credentials(client)
employment = Enum.find(creds, &(&1["id"] == chosen_id))
Layr8.Client.send_message(client, %Layr8.Message{
to: [peer],
type: "https://layr8.io/protocols/mcp/1.0/tools-call",
body: %{"params" => %{"name" => "place_order"}},
attachments: [Layr8.Identity.attachment!(employment["credential_jwt"])]
})Attaching one does NOT cost the message its grants: the client appends the wallet's selection after attachments that are all identity credentials.
The same abstraction in the other SDKs
attachment/1 and attachment?/1 are this SDK's spelling of the node SDK's
identityAttachment / isIdentityAttachment, the Python SDK's
identity_attachment / is_identity_attachment, and the Go SDK's
IdentityAttachment / IsIdentityAttachment. Same argument, same two refusal
cases; only the name is module-qualified, as Elixir names are.
Summary
Functions
Build the attachment that carries one identity credential.
attachment/1, raising ArgumentError instead of returning {:error, _}.
Is this attachment an identity credential — the same test the node routes on, applied to what is actually on the message?
The only media type the node's credential extractor keeps, matched by exact string equality.
Types
@type reason() :: :not_compact_jws | :credential_is_grant
Why a credential could not be attached as an identity credential.
:not_compact_jws— not three non-empty dot-separated segments, or a payload segment that is not base64url-encoded JSON object. The node can verify nothing else, so putting it on the wire only buys a denial that names the wrong problem. Three segments is not the same as three READABLE segments: a payload that does not decode says nothing aboutcredentialSubject.scope, so nothing here can show it to be an identity credential rather than a grant.:credential_is_grant— a non-emptycredentialSubject.scope. That is a GRANT: the node would route it to the policy'scredentialsinput, it would not satisfy asenderCredentialsrequirement, and the resulting denial is indistinguishable from having attached nothing. The check is local, exact and free — refusing here is the difference between an error at the call site and a silent misroute diagnosed at the far end. Grants belong toLayr8.Wallet, which selects and caps them; this path does neither.
Functions
@spec attachment(String.t()) :: {:ok, Layr8.Attachment.t()} | {:error, reason()}
Build the attachment that carries one identity credential.
credential_jws is the credential itself — the compact JWS, which
Layr8.Client.list_credentials/2 returns as "credential_jwt". It is not a
credential id: an id would mean a read from the node, and this runs inside the
send path where an unbounded read stalls the client (the reason
Layr8.Config's grant_read_timeout_ms exists). A JWS also lets a caller
attach a credential the node's store has never seen.
Returns {:error, reason} rather than an attachment the far end will misread.
See reason/0.
@spec attachment!(String.t()) :: Layr8.Attachment.t()
attachment/1, raising ArgumentError instead of returning {:error, _}.
The shape for a call site that is naming a credential it already read from the node, where a refusal is a programming mistake and not a runtime condition.
@spec attachment?(Layr8.Attachment.t() | any()) :: boolean()
Is this attachment an identity credential — the same test the node routes on, applied to what is actually on the message?
Used by the client to decide whether caller-supplied attachments should still displace the wallet. Nothing here trusts how the attachment was built: a hand-assembled one counts exactly the same.
False for an attachment whose jws does not decode, as well as for one that
carries a scope. Only a credential this can actually READ as scope-free is an
identity credential.
The only media type the node's credential extractor keeps, matched by exact string equality.
Everything else — including application/vp+jwt, the Verifiable Presentation
envelope — is dropped in silence, and the denial that follows is byte-for-byte
the one for attaching nothing at all.