matrix-component (Elixir SDK)
Experimental Matrix SDK for Elixir (ML1 contract surface, version
0.1.0). Dual-licensed MIT OR Apache-2.0 (see LICENSE-MIT,
LICENSE-APACHE-2.0); Hex publication in progress.
Contract reference: docs/SDK.md, docs/ML1-NODE.md,
docs/ML1-MATRIX.md in the Matrix repository. Same observable
behavior as the reference SDKs (Rust matrix-component, Python
matrix_component).
Install
No dependencies beyond Elixir/OTP stdlib (:json needs OTP >= 27).
Elixir >= 1.17 (tested 1.20 with OTP 29, linux/x86_64):
# offline: copy the SDK dir (or scaffold, which stages it)
{:matrix_component, path: "/path/to/staged/sdk/elixir"}
mix deps.get fetches nothing; mix test and mix escript.build
work offline.
Component side
defmodule Echo do
use Matrix.Handler
@impl true
def on_call(ctx, _ticket, _cap, input, _ref) do
if input["chain"] == true do
[first | _] = Matrix.CallCtx.dependencies(ctx)
out = Matrix.CallCtx.invoke_dependency(ctx, first.id, input["input"] || %{}, 5_000)
{:ok, %{"chained" => out}}
else
{:ok, %{"echo" => input}}
end
end
end
{:ok, reader} = Matrix.Component.connect(System.get_env("MATRIX_SOCK"), "echo")
Matrix.Component.serve(reader, Echo) # :dispose | :eof
- One supervised task per call; cancel arrives as
{:matrix_cancel, ticket}in the task mailbox (CallCtx.cancelled?/1polls without consuming other messages) pluson_cancel/1. Late answers after cancel stay silent. Handlers return{:ok, output}or{:error, code, message}, or raiseMatrix.Errors.BusinessError. send_stream(ctx, id, seq, text): text only — non-UTF-8 binaries raiseinvalid-message, never lossy-converted.u64wire values are decimal strings compared withMatrix.Framing.gen_equal?(full precision; out-of-range is stale/invalid, never wrapped).- Events/streams share a bounded edge queue in the reader (64,
drop-oldest, counted in
event_dropped_count/1); the dispatcher process pulls batches, so slow observers throttle via host credit instead of stalling calls. - Without a negotiated
dependency-calls/1,invoke_dependencyraisesDepError(unsupported-feature) without touching the wire. - Supervision: the reader is linked to the connecting process — its death takes the session down (no orphan context), and the SDK never resurrects an old generation (documented difference from auto-restart supervisors, same guarantee).
Operator side
kernel = Matrix.Operator.start("/path/to/matrix-managed", config, opki)
act = Matrix.Operator.activate(kernel.client, "prov", 20_000)
v = Matrix.Operator.invoke(kernel.client, act["lease"], act["fence"],
"op-1", "prov.echo@1", %{"ping" => 1})
Matrix.Operator.close(kernel) # owned: reaps only this daemon
Operator calls travel through the staged matrix-managed binary
(serve/request, mutual TLS). start owns its daemon (SIGTERM,
bounded wait, SIGKILL — Port.close/1 alone never signals);
connect attaches (closing never stops a shared kernel). The
operator PKI map is always explicit. Timeouts report
outcome-unknown, never retry.
Scaffold, doctor, tests
./scaffold.sh myapp ./myapp --pki ./pki --home ./priv-home # stages SDK, builds escript node
mix test # loopback suites; live parts need MX_MATRIX_MANAGED + MX_DEV_PKI
mix run -e 'Matrix.Doctor.main()' -- --binary /path/to/matrix-managed
Matrix.Doctor prints environment diagnosis as JSON and redacts
secrets. Scaffolded projects are covered by templates/README.md.