Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

macula-py

CI License Python GitHub Sponsors Buy Me a Coffee

Macula

A Python node on the macula mesh, over macula-go's C ABI


Wire 1.0, 0.6.0rc4 (a pre-release): built on macula-go v0.27.0-rc.8, it speaks wire 1.0 (macula 15) and nothing older, so it reaches a wire 1.0 station and never the 14.x fleet. This candidate adds sealed group pub/sub, the member's side: a Python node joins a realm group, pulling its epoch keys from the org's distributor over a sealed call with the org's grant, and publishes and reads the group's sealed events. A pre-release is not what a plain pip install macula-py resolves: 0.5.0, on the macula 12 wire, stays the default until wire 1.0 ships on the fleet. Install the candidate with pip install --pre macula-py or pip install macula-py==0.6.0rc4.

Status of 0.5.0, 2026-09-29: that line, the one a plain install resolves, on the macula 12 wire: ML-DSA-87 identities (as the ML-DSA-87 + RSA-PSS-4096 composite in pq_hybrid, the fleet's profile), ML-KEM hybrid key exchange, signed requests, and since 0.4.0 handshake v5 (the session bound to its TLS channel) and macula 13's end-to-end sealing with the caller's seal report, checked live against macula 13.2.2 both ways. Calls and streams by direct dial, serving (under an org or in a node's own namespace, open or gated on a post-quantum UCAN), publish/subscribe, the DHT and node-served content are tested against two in-process macula 12 stations on every CI run. Device request proofs (realm join) and ownership proofs are held to their verifiers' vectors, and checked once through the realm's and mcl_om's own verifiers. 0.1.0 spoke the retired classical wire and cannot reach the current fleet.

What is this?

A Python SDK for the Macula mesh: a node's key, a pool of links to stations it pins by node_id, calls and streams that reach a provider by direct dial, serving procedures, publish/subscribe, the DHT and node-served content, all as asyncio coroutines.

Macula is a federated mesh for sovereign application networks. A station relays and holds the DHT; a node is anything else that joins, and this package is a node.

It is a binding, not a reimplementation. The protocol lives in macula-go, which exports it as a C ABI (cabi/macula.h, contract in cabi/CONTRACT.md) shared by every SDK not written in Go. macula-py loads that library with ctypes, so the wire, the post-quantum handshake and the signing rules are the ones macula-go already checks byte for byte against macula itself.

Install

pip install macula-py

Wheels exist for Linux x86-64 and arm64 (glibc 2.28 or later), macOS 13 or later on Apple silicon and Intel, and Windows x86-64. Each carries macula-go's library; nothing compiles at install time and there are no runtime Python dependencies. On any other platform pip finds no wheel.

Quick start

import asyncio

from macula_py import NodeKey, Pool, Seed

STATION = Seed("station-fi-helsinki.macula.io", 4433,
               "004d1f470097ccf8826ce291900e882fdb1f20375e53901facaec0f23eb4efd8")
REALM = "abb81b5a614b63551b400b810648c0c8a78efad845442630c94b46cc95d2fcd1"  # io.macula
REALM_KEY = "..."  # io.macula's public realm key, hex


async def main() -> None:
    key = await NodeKey.load_or_create("node.key")  # pq_hybrid; the puzzle takes a second
    async with await Pool.connect(key, [STATION], realm_trust={REALM: REALM_KEY}) as pool:
        print(await pool.call(REALM, "mcl-echo/echo", "hello"))


asyncio.run(main())

A seed is pinned: the station must prove the node_id you give. A realm's key decides which advertisements in it you trust; procedures in a node's own namespace (~<node_id>/<name>, see Pool.own_procedure) need none.

The API

NodeKey generate, load, load_or_create, save, node_id, public_key, profile, sign, verify, free; ucan, device_request_proof, ownership_proof (below)
Pool.connect seeds, realm_trust, kem_advertise (sealing, below), and the pool's tuning; async with closes it
calls call, call_report, providers; call(..., ucan=, proofs=) presents a UCAN, confidential= seals it
serving serve(realm, procedure, handler, policy=None, confidential=None): handler(request) returns the result, directly or as an awaitable; an exception reaches the caller as a ProviderError of code handler_error
streams open_stream, serve_stream; a Stream has send, send_value, close_send, reply, abort, close, recv, report, and iterates its frames
pub/sub publish, subscribe, and publish_group, subscribe_group for a sealed group (below); a Subscription iterates its events, or next(timeout_ms)
content share_content, unshare_content, get_content
DHT find_record, find_records, find_records_by_type, put_record

UCANs

A procedure served with a policy answers only callers presenting a UCAN (wire 1.0's post-quantum capability token) the policy accepts; the provider checks each call and stream open before the handler sees it, as macula does, and answers the rest unauthorized (or malformed_frame for a proof no token in the chain names):

from macula_py import UcanRequired
from macula_py.ucan import proof_id

served = await provider.serve(realm, procedure, handler, policy=UcanRequired(root.node_id()))

token = root.ucan(caller.node_id(), [{"with": "mri:org:io.macula/acme", "can": "invoke"}],
                  exp=int(time.time()) + 3600)
await caller.call(realm, procedure, payload, ucan=token)

# Delegated: alice hands the caller one procedure, naming her grant as its parent.
sub = alice.ucan(caller.node_id(), [{"with": "mri:proc:io.macula/acme/count_v1", "can": "invoke"}],
                 exp=int(time.time()) + 600, prf=[proof_id(to_alice)])
await caller.call(realm, procedure, payload, ucan=sub, proofs=[to_alice])

A token is minted for the node that will present it. RealmMemberRequired(key_id, can) gates on a realm key instead, named by macula_py.ucan.key_id(realm_public_key, profile) (the key as carried, bytes). macula's test/vectors/UCAN_V1.md is the contract.

Sealing

macula 13 seals a call's or a stream's payload end to end to the provider's KEM key (E2E seal scheme 1): stations route what they cannot read. It is off until a provider opts in, and a caller seals whenever it can:

# The provider names its KEM key in its advertisements.
provider = await Pool.connect(key, seeds, realm_trust=trust, kem_advertise=True)
served = await provider.serve(realm, procedure, handler, confidential="required")

# The caller seals to it: "preferred" (the default) whenever the provider's
# advertisement names a key, "required" never calls one that names none.
result, report = await caller.call_report(realm, procedure, payload, confidential="required")
assert report.sealed and report.provider == provider.node_id()  # seal_key_id: the key, 16 hex
  • kem_advertise is off by default. Enable it only once every station runs macula 12.11 or later and every caller can seal (macula 13, macula-go 0.18, macula-py 0.4 or later).
  • A served procedure is confidential="preferred" by default: it names the key when the pool advertises one, and still takes a clear call while its last keyless advertisement could be served. "required" refuses every clear call (sealed_required) and needs kem_advertise; "off" serves in the clear. request.sealed says whether a call came sealed; its payload is the opened plaintext either way.
  • A caller cannot ask for "off" (InvalidArgumentError): only an advertisement naming no key is called in the clear, and a sealed call never falls back to the clear. What could not be kept confidential raises ConfidentialityError, its reason one of no_kem_key, key_mismatch, reply_not_opened, clear_answer_to_sealed, kem_advertise_disabled.
  • The seal report states that sealing ran on the exchange behind a result, nothing more. A stream's, stream.report(), settles on the provider's first chunk or reply; before that it raises NotSettledError, and on a served stream NotACallerError.

What stays visible: a request's UCAN and proofs, sizes, timing and routing. Content (share_content) is public by design and travels in the clear. macula-go's cabi/CONTRACT.md ("Confidentiality", "The seal report") is the contract.

Sealed groups

A realm group is a topic prefix whose second segment is the org that owns it (<realm_name>/<org>/<name>). The org's distributor serves the group's epoch keys at <org>/group_keys_v1; a member pulls them over a sealed call, presenting the org's grant (a UCAN whose can is group_keys), and a member's publication under the prefix is sealed to the epoch it then holds.

from macula_py import Group

group = Group("io.macula/acme/chat", ucan=org_grant)

await publisher.publish_group(REALM, "io.macula/acme/chat/said_v1", "hello", group)
subscription = await reader.subscribe_group(REALM, "io.macula/acme/chat/said_v1", group)

event = await subscription.next()
assert event.sealed and event.payload == "hello"
print(event.seal_key_id)  # the epoch it opened under, 8 bytes as hex
  • Each call joins the group first, pulling its epoch from the distributor over a sealed call, so each takes a timeout like a call.
  • A clear event under the prefix is not refused for you. The library refuses one only under a group whose own policy is required, and the distributor sets that policy, not the subscriber; under any other policy a clear event any realm member published under the prefix arrives as it is, with Event.sealed false and a payload. An app that wants sealed events alone checks Event.sealed; nothing in the binding can decide that for it.
  • Event.sealed and Event.seal_key_id name the epoch a member's event was opened under, and an event that arrived sealed but could not be opened still carries Event.seal_key_id. One that cannot be opened arrives once with payload None and Event.unopened naming why; a plain subscribe under a group prefix hears every event as no_group. An event whose payload is genuinely null also reads as payload None, so unopened is what tells the two apart.
  • A refusal, or a publication that cannot be sealed, raises GroupError. Its reason is one of macula's closed set (not_a_member, membership_unknown, unknown_epoch, epoch_expired, no_distributor, no_group, tag_invalid), and group_held, a clear publish under a prefix this node holds a key for, also names the group's prefix.
  • distributor= names the node that must be the group's, required for a ~<node_id> group; a prefix with no second segment, or a group that is not a macula_py.Group, is refused before it leaves.
  • Serving group keys is the org's, not this SDK's: macula's own macula_group_keys, or yours.

Proofs for a realm and for a service

NodeKey.device_request_proof(realm, procedure, request, rule) signs a device's request to a realm (realm proof v2): a join session's body (rule="http", a mapping or the body text exactly as sent) or a membership UCAN request over the mesh (rule="mesh"). NodeKey.ownership_proof(realm, procedure, payload) returns the payload with the asserted_by block that authorises its fields to a service such as mcl_om; send it as the payload. Neither signs a "caller": the caller is the verified signer, so a request or payload carrying one is refused.

Payloads are what macula's wire carries: str, int within int64, float, None, bytes, lists and dicts with str keys. There is no boolean on the wire: send 1 and 0. A Python bool is refused before it leaves, since Python treats it as an int.

Every networked method is a coroutine, and each native call runs on a thread of its own, so long waits never starve other calls. The methods that take timeout_ms, and every wait for an event, a served call or a stream frame, carry a cancel token: cancelling the task (or asyncio.wait_for timing out) ends the native call at once. The rest (publish, subscribe, serve, stop, close, and a stream's sends and ends) are short native calls without one; cancelling stops the waiting and the call runs to its end.

Errors are typed: ProviderError, RelayError, StreamError, NotSharedError, ContentUnavailableError, NoProviderError, MaculaTimeoutError (also a TimeoutError), InvalidArgumentError (also a ValueError), ClosedError, RefusedError, ConfidentialityError, GroupError, NotSettledError, NotACallerError, all MaculaError. A station's refusal of a serve or subscribe is a RefusedError with its reason, the registration (advertise or subscribe) and the station's node_id.

Development

Needs Go 1.27 and a C compiler to build macula-go's library locally.

python -m venv .venv && .venv/bin/pip install -e ".[dev]"
eval "$(scripts/build_native.sh)"   # the library and teststation, from abi/MACULA_GO_REF
.venv/bin/pytest

abi/macula.h is macula-go's header at the ref in abi/MACULA_GO_REF; tests/test_abi_declarations.py holds the ctypes declarations to it function for function, and build_native.sh refuses a header that differs from the ref's.

scripts/live_check.sh runs tests/live/ against one fleet station (helsinki and io.macula by default) with keys made for the run and never saved. It publishes once, and advertises one UCAN-gated procedure in its own namespace under a throwaway realm. CI never runs it.

scripts/interop/ownership_proof.sh and scripts/interop/device_request.sh check proofs this binding signs against the verifiers themselves: mcl_om's (in macula's pinned CI image), after the payload has crossed a station as a provider receives it, and the realm's. scripts/interop/v5.sh and scripts/interop/sealed.sh run handshake v5 against a macula station, and sealed calls and streams with their seal reports both ways against a macula node, and scripts/interop/groups.sh sealed groups both ways against macula's own distributor, an Erlang member and a Python member each opening the other's sealed event under one epoch. See scripts/interop/README.md.

A v* tag publishes to PyPI through Trusted Publishing, using only macula-go's released libraries, each checked against the release's SHA256SUMS and its build provenance attestation.

License

Apache-2.0. See LICENSE.


Built on macula-go, for Python -- sponsor the work if this saved you some time

Metadata

Release files for macula-py 0.6.0rc4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Built distributions (wheels)

Table of built distributions (wheels) for macula-py 0.6.0rc4
File
macula_py-0.6.0rc4-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
macula_py-0.6.0rc4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl Python 3 none Linux glibc 2.28+ ARM64, Linux glibc 2.17+ ARM64 Details
macula_py-0.6.0rc4-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64, Linux glibc 2.5+ x86-64 Details
macula_py-0.6.0rc4-py3-none-macosx_13_0_x86_64.whl Python 3 none macOS 13.0+ x86-64 Details
macula_py-0.6.0rc4-py3-none-macosx_13_0_arm64.whl Python 3 none macOS 13.0+ ARM64 Details

Total release size: 17.6 MB

Release files / macula_py-0.6.0rc4-py3-none-win_amd64.whl

Download URL macula_py-0.6.0rc4-py3-none-win_amd64.whl
Size 3.6 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
baa446499351029f9f5295e031bbcd005428c1f226eaa46d8f3261a91d663fca
BLAKE2b-256 checksum
How to use checksums
3c09527f1ac289490632765f900a99a9746bf232ecb77c77286c056d2598004d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / macula_py-0.6.0rc4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl

Download URL macula_py-0.6.0rc4-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl
Size 3.4 MB
Tags Linux glibc 2.17+ ARM64 Linux glibc 2.28+ ARM64 Python 3
SHA-256 checksum
How to use checksums
a0b742370286b835afbe23d1177c4f9ade55d4a8d360aad22fa3e888ab1ab400
BLAKE2b-256 checksum
How to use checksums
7bd9fe8df33dd6f369ec2bdefc1d013a18a6d5a697e5bda8cc2bde5033ab2f6d
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / macula_py-0.6.0rc4-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl

Download URL macula_py-0.6.0rc4-py3-none-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl
Size 3.7 MB
Tags Linux glibc 2.28+ x86-64 Linux glibc 2.5+ x86-64 Python 3
SHA-256 checksum
How to use checksums
93c4283436d1e4eda35b9ae5bf65052b987a75c0857c2642f9398438beda4e45
BLAKE2b-256 checksum
How to use checksums
765a33696986d6b02fffe7d8c9776a0d094c3b54021be173ebe1548fb6609771
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / macula_py-0.6.0rc4-py3-none-macosx_13_0_x86_64.whl

Download URL macula_py-0.6.0rc4-py3-none-macosx_13_0_x86_64.whl
Size 3.6 MB
Tags Python 3 macOS 13.0+ x86-64
SHA-256 checksum
How to use checksums
855b523f861c59ba48592b40075f6a7423650e986097b19b509ed52dc2ed1a1e
BLAKE2b-256 checksum
How to use checksums
a0b7d2491c96b9dd90a2f09795519529ed55a0f9eb13bfb0ade2285d58cefabd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log

Release files / macula_py-0.6.0rc4-py3-none-macosx_13_0_arm64.whl

Download URL macula_py-0.6.0rc4-py3-none-macosx_13_0_arm64.whl
Size 3.3 MB
Tags Python 3 macOS 13.0+ ARM64
SHA-256 checksum
How to use checksums
8aae01792ae1a031ecbe7337dd8a8b8d71064b6cc203a33dfa4e2f0bbfd58893
BLAKE2b-256 checksum
How to use checksums
96843fbd1a0bfab50b42cd88b7f47acdbc80a98581d557b67cc12955ebe1d381
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 17, 2026.

Transparency log
Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page