Current section

Files

Jump to
layr8 CHANGELOG.md
Raw

CHANGELOG.md

# Changelog
All notable changes to `layr8`. Format loosely follows [Keep a Changelog](https://keepachangelog.com/); versioning follows [SemVer](https://semver.org/).
This file starts here. Earlier releases (through 0.2.8) are recorded only in git history.
## [0.2.10] - 2026-08-17
### Added
- **More than one DID on a single connection**`Layr8.Client.join_did/3`
joins an additional `plugins:<did>` topic on the WebSocket `connect/1`
already opened, and returns a `Layr8.DidHandle` that sends and requests as
that DID. Until now one client meant one DID, so an agent speaking for many
identities (a workflow per DID, an account per DID) needed one connection
each; the cloud-node's plugin socket has always matched `plugins:*`, so the
limit was the SDK's alone.
Each joined DID carries its own handlers (`:handlers`, `:handle_all`), its
own protocol subscription (`:protocols`) and its own `:did_spec` — including
`controller`, which decides whose grants can cover it. The client-global
registry stays the fallback for every DID. Joined DIDs are re-joined
automatically after a reconnect.
New API: `Layr8.Client.join_did/3`, `leave_did/2`, `joined_dids/1`,
`send_from/4`, `request_from/4`, `request_result_from/4`, and the
`Layr8.DidHandle` module. Existing single-DID use is unchanged.
Modelled on the node-sdk's `joinDid` / `DidHandle`, which mcp-pod uses in
production to host one Instance DID per connected account.
- **A directory-visible check that the node really does host many DIDs** — a
`multi_did` compat scenario. `join_did`'s three load-bearing properties belong
to the cloud-node, not to this library, so no unit test can establish them:
that the node accepts more than one topic on one socket, that an inbound frame
reaches the handler of the DID it was addressed to, and that after the socket
drops every joined DID comes back by itself. The last is the one worth having:
losing an extra DID on a reconnect is silent, with nothing to observe on this
side.
### Fixed
- **`joined_dids/1` no longer reports a DID the node has not acknowledged.** It
answered from the client's handler table, which survives a dropped socket
untouched, so across a reconnect it listed DIDs whose re-join was still in
flight and a send to one of them raised `NotConnectedError`. Measured against a
real node: at the moment `on_reconnect` fired, the re-join replies were still
60ms and 100ms away. It now answers from the channel, which tracks what the
server confirmed. Never shipped — `joined_dids/1` is new in this release.
## [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/3`**`request/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.
[0.2.9]: https://github.com/layr8/elixir_sdk/releases/tag/v0.2.9