All notable changes to layr8. Format loosely follows Keep a Changelog; versioning follows SemVer.

This file starts here. Earlier releases (through 0.2.8) are recorded only in git history.

[Unreleased]

0.2.9 - 2026-08-10

Added

  • Verifiable Grants are attached to outbound messages — automatically, on every send path (send/3, request/3, and a handler's reply). The cloud-node requires a grant for anything its policy does not allow outright, and nothing in this SDK attached one: an agent that connected directly sent nothing and was denied with "no grant covers this call", a message that reads as "your grant is misconfigured" when the truth is "no credential was ever put on the wire".

    Layr8.Wallet reads the holder's credentials from the node, caches them for :grant_cache_ms (default 60s) and selects the covering set with a mirror of helix's structure_v2.rego. Caller-supplied attachments are never displaced, and a wallet failure never blocks the send.

    New config: :attach_grants (default true, env LAYR8_ATTACH_GRANTS), :grant_cache_ms, :grant_read_timeout_ms, :on_grant_miss. New API: Layr8.Client.refresh_grants/2.

    Contract: contracts/sender-cn-vg-attachment.md.

  • :on_grant_miss — told when the node denied a message that went out with nothing attached, when the covering set had to be capped at 16, or when the grants could not be read at all. It deliberately stays quiet on "nothing covered this message" alone: most traffic (discovery, trust-ping, problem reports) needs no grant.

  • Layr8.Mcp — MCP over DIDComm. Layr8.Client.mcp/2 registers the protocol subscription and returns a binding; Layr8.Mcp.peer/2 yields a caller with initialize/3, list_tools/2 and call_tool/4. It handles the tools/call#{base}/tools-call type mapping, the JSON-RPC envelope and unwrapping result. Must be called before connect/1, like handle/3. Contract: contracts/mcp-over-didcomm.md.

  • Layr8.Client.request_result/3request/3 without the raises, for callers that route on failure rather than rescuing it. Layr8.Mcp uses it, which is why its whole surface is tagged tuples.

  • :rest_timeout_ms (default 30s, env LAYR8_REST_TIMEOUT_MS) — a deadline on every credential and presentation call, not just the grant read. Req has none of its own, so a node that accepted the connection and went quiet left those calls hanging. 0 disables it.

Changed

  • Every send now performs a credential read against the node before the message goes out (once per :grant_cache_ms per DID; failures are cached for a shorter window so a misconfigured API key is not a per-message round trip). A node that cannot serve /api/v1/credentials degrades to sending unattached — the previous behaviour — and :on_grant_miss reports it. Set attach_grants: false to opt out entirely.

  • Layr8.REST no longer retries. Req retries safe requests by default with a 1s/2s/4s backoff, and because receive_timeout is per attempt, that silently multiplied every deadline by four: measured, a 2s grant read against a node that accepts the connection and goes quiet took 7.6 seconds to return. A deadline exists here so a hung node cannot stall the sends queued behind it, so retry: false now goes wherever a deadline goes. Retrying is the caller's decision, with its own knowledge of whether the call is worth repeating.

  • Layr8.REST.new/2, get/2 and post/3 gained optional trailing arguments; the existing arities are unchanged.

Fixed

  • Layr8.Attachment's documentation pointed at the wrong field. It said to put application/vc+jwt in format and to carry credentials in data.base64. media_type is the ONLY field the node's credential extractor filters on, by exact string equality, and it drops everything else before looking at the data — producing a denial byte-for-byte identical to the one for attaching nothing. Following those docs attached nothing at all. The same correction is on Layr8.Presentations.sign_presentation/3, which is not the authorization path either.