----- index.md -----
# ours-mufl-core
**[ours.network](https://ours.network) is free, source-available infrastructure for secure
agent-to-agent communication.** `ours-mufl-core` is the protocol at its heart: the shared
wire format and verification logic, written as MUFL (`.mm`) libraries and compiled into
every client — so all of them speak identical bytes.
> **These docs are written for agents.** ours.network expects most applications to be
> built by coding agents working on behalf of a human. Pages are structured as executable
> runbooks — exact paths, copy-paste commands, verification steps — rather than narrative
> tutorials. Humans are welcome; give your agent the URL of
> [`llms-full.txt`](https://adapt-toolkit.github.io/ours-mufl-core/llms-full.txt) and it can ingest the entire
> documentation in one fetch.
## Three ways to consume this site
1. **Agent, bulk** — fetch [`llms-full.txt`](https://adapt-toolkit.github.io/ours-mufl-core/llms-full.txt) (everything,
one file) or [`llms.txt`](https://adapt-toolkit.github.io/ours-mufl-core/llms.txt) (annotated index).
2. **Agent, targeted** — every page is pure Markdown in
[`docs/` on GitHub](https://github.com/adapt-toolkit/ours-mufl-core/tree/main/docs);
fetch raw files directly.
3. **Human** — browse: [How it works](/how-it-works/overview) ·
[Build your own app](/guide/) · [Reference](/reference/modules).
These docs cover **using** the protocol — understanding it and building applications on
it. Real apps built on the core: see
[reference implementations](/reference/implementations).
----- how-it-works/overview.md -----
# Overview
ours-mufl-core is the shared wire format and verification logic for the ours.network protocol,
written as MUFL (`.mm`) libraries. Every ours.network client — agent server, web messenger,
Telegram connector — vendors this repo as a git submodule and compiles the same libraries into
its own packet. Because all clients link the same code, they speak an **identical wire format**
and verification logic: a packet built by the web client and one built by the MCP agent are
byte-compatible peers.
The host application boots packets, routes inbound messages, and persists state. The protocol
libraries define what is valid; the host only moves bytes.
## Consuming the core
Add the repo as a submodule to your application:
```sh
git submodule add git@github.com:adapt-toolkit/ours-mufl-core.git mufl_code/core
```
Your `config.mufl` loads it with `config_load #"core"`. The full setup is in
[01 · Vendor the core](../guide/01-vendor-the-core.md).
## Modules at a glance
Seven `.mm` libraries and one config export. See [Modules](../reference/modules.md) for
per-module purpose and source links.
| File | Role |
|------|------|
| [`a2a_protocol.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_protocol.mm) | Wire shapes (invites, certs, profiles) and verification helpers |
| [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm) | Invite, contact, send/receive message, and send/receive file transactions |
| [`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm) | App manifest, verb dispatch, well-known capabilities |
| [`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm) | Child lifecycle, host-local contact book, cluster management |
| [`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm) | Monitoring copy receiver (CP side) |
| [`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm) | Control-plane transport |
| [`version.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/version.mm) | Core version, readable at runtime via `get_core_version` |
Source: [`README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/README.md).
## Next steps
- [Identity: roots & roles](./identity.md) — delegation hierarchy and verification
- [Invites & contacts](./invites-and-contacts.md) — ephemeral-key invite and redeem flow
- [Messaging](./messaging.md) — send, receive, and contact-restore
----- how-it-works/identity.md -----
# Identity: roots & roles
An identity in ours.network is either a **root** — a self-sovereign keypair — or a **delegated
role** anchored to a root. A root's `delegation_cert` field is `NIL`; detection is structural,
not a flag. A role carries a signed `delegation_cert_t` that binds the role's container id and
address-document hash to its root, signed by the root's keys.
Source: [`a2a_protocol.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_protocol.mm).
## Delegation verification
When a peer presents its identity during invite redemption, verify the full chain with
`verify_peer_delegation`:
```
fn verify_peer_delegation (
peer_cid: global_id,
peer_ad_hash: hash_code,
cert: delegation_cert_t,
rp: root_profile_t
) -> contact_root_t
```
The root profile carries the root's key list, so no prior knowledge of the root is needed. On
success the function returns a `contact_root_t` (root container id, root name, role id) to store
beside the contact. Any mismatch — wrong version, mismatched cid, bad signature — aborts.
## Control-plane governance (§3c)
Two verifiers support the optional non-enforcing governance edge between a root and its control
plane:
- **`verify_cp_attestation`** — verifies the CP's signed attestation that it governs a given
root. Requires the caller to have run `process_address_document` on the CP's address document
first.
- **`verify_root_cp_binding`** — verifies the root's self-signed edge naming the CP, domain-
separated by `cp_attestation_context_tag`. The authoritative monitoring gate remains the
6-digit bind ceremony; these verifiers let peers TOFU-pin the governance edge without an extra
round-trip.
Source: [`a2a_protocol.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_protocol.mm).
## Wire shapes and the structural-records rule
All identity wire shapes are MUFL record types defined in `a2a_protocol.mm`. The key protocol
guarantee is: moving a shape between libraries never changes its bytes; renaming or retyping a
`$field` does. Field names are the version boundary. Example shapes:
```
metadef delegation_cert_t: ($c -> delegation_core_t, $s -> crypto_signature).
metadef root_profile_t: ($p -> root_profile_core_t, $s -> crypto_signature).
metadef root_cp_binding_t: ($c -> root_cp_binding_core_t, $s -> crypto_signature).
```
The invite shape carries no identity keys — identity material moves inside the encrypted
two-message redeem hop (see [Invites & contacts](./invites-and-contacts.md)).
----- how-it-works/invites-and-contacts.md -----
# Invites & contacts
Contact establishment uses a **slim ephemeral-key invite**. The invite blob carries
only an ephemeral encryption pubkey, the inviter's container id, a display name, and a crypto
scheme id — no long-term keys, no self-signatures. Identity material moves inside two encrypted
messages exchanged after the initial out-of-band transfer.
Source: [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm).
Sequence diagrams: [Contact exchange](../workflows/contact-exchange.md) and
[Introductions](../workflows/introductions.md).
## Invite shape
```
metadef invite_eph_t: (
$d -> global_id, -- invite id (correlates legs; single-use)
$c -> global_id, -- inviter container id
$n -> str, -- inviter display name
$k -> publickey_encrypt, -- inviter ephemeral pubkey
$v -> int -- crypto scheme id
).
```
Generate an invite with `generate_invite`. The returned blob is the value to share out-of-band
(QR code, deep link, etc.).
## Three-leg redeem flow
| Leg | Direction | Mechanism | What moves |
|-----|-----------|-----------|------------|
| OOB | inviter → responder | out-of-band | slim `invite_eph_t` blob |
| Leg 1 | responder → inviter | BARE send (box to invite's eph pub) | responder identity bundle |
| Leg 2 | inviter (receives leg 1) | verify bundle, consume invite, emit leg 3 | ← inviter now holds responder's verified AD |
| Leg 3 | inviter → responder | BARE send (box to responder's eph pub) | inviter identity bundle |
Both boxes carry the same identity bundle shape: address document + optional delegation cert +
optional root profile + optional §3c CP binding. After leg 3, both sides hold the other's
verified address document and `encrypted_channel` resumes for all subsequent traffic.
Redeem an invite with `add_contact` (leg 1). The inviter processes the responder's leg 1 via
`submit_invite_response` and replies with leg 3 (`complete_invite`).
## Single-use guarantee
An invite is consumed at the first valid leg-2 (inviter's receipt of leg 1). A second attempt for the same
`invite_id` aborts with `already-redeemed` and mutates no state. The ephemeral private key is
deleted atomically with the invite record (INV-4: secrets are never exported).
Test suite scenario T3 (`single-use`) asserts a second leg-1 for the same `invite_id` aborts
and leaves inviter state unchanged. Scenario T4 (`invalid-then-valid`) asserts that a failed
box-open does not consume the invite, so a subsequent valid redeem succeeds. See
[`tests/README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/README.md).
## Pending-invite transience
Responder-side pending redemption state (`pending_redemptions`) is not exported and does not
survive a daemon restart. An interrupted handshake does not leave half-registered state; the
responder must run `add_contact` again with a fresh invite. This is the fail-closed design: no
dangling partial contacts after a restart.
## Introductions via core.connect
When two nodes share a control plane that advertises `core.connect`, the CP can introduce them
without another out-of-band invite. The CP sends each node the other's signed address document
via `ingest_connect_descriptor`. The receiving node verifies:
1. The relay came from its bound CP (or the CP its root designated).
2. Its own live manifest advertises `core.connect`.
3. The peer's address document self-signature is valid (proof-of-possession).
The contact is registered immediately — no SAS, no confirmation step.
Scenario T1 (`happy-flat`) verifies the end-to-end invite + `send_message` round-trip in both
directions. Scenarios T5–T8 cover adversarial inputs (tampered box, cid-bind mismatch, stripped
PoP, unexpected inviter on leg 3). See
[`tests/README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/README.md).
----- how-it-works/messaging.md -----
# Messaging
All message and file traffic between contacts rides the `encrypted_channel`, established during
invite redemption and persisted across restarts by replaying stored peer address documents on
import. `a2a_messaging.mm` is the single path for all send/receive operations.
Source: [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm).
Module description: [`README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/README.md)
— "Contact and messaging transactions (generate invite, add/remove contact, send message, send
file, inbound receive) and the introduction flow."
Sequence diagrams: [Send & receive messages](../workflows/messaging.md) and
[Contact restore](../workflows/contact-restore.md).
## Outbound
- **`send_message`** — sends text to a named or container-id-referenced contact. Accepts an
optional `reply_to` pointer (`$wire_id`, `$sentence`) for threaded replies. Every message
receives a stable `wire_id` (a stringified `_new_id`) that the peer can reference in its own
replies.
- **`send_file`** — sends binary data with a `$filename` and optional `$mime`. Files and text
messages share the same `wire_id` namespace; a reply pointer can reference either.
Both fail if the contact has no registered address document (see Contact-restore below).
## Inbound
| Transaction name | Source constant | Payload |
|-----------------|-----------------|---------|
| `::actor::receive_message` | `receive_message_tx` | `$text`, `$wire_id`, optional `$reply_to`, sender id from envelope |
| `::a2a_messaging::receive_file` | `receive_file_tx` | `$filename`, `$mime`, `$data`, `$wire_id`, optional `$reply_to` |
The core fires the app-injected `on_message_received` / `on_file_received` storage hooks.
Message storage is the consumer's responsibility; the core handles wire, validation, and contact
resolution.
## Receiver-side state
`contacts` (keyed by container id) and `peer_ads` (keyed by container id) are the persistent
contact state. `encrypted_channel` resolves the peer's encryption key from `peer_ads` on every
send. `peer_ads` entries survive code upgrades because `import_core_state` replays each stored
address document through `process_address_document` — no re-handshake is needed.
## Contact-restore across breaking changes
If a migration carries `contacts` but drops `peer_ads` for a contact, that contact becomes a
**DEGRADED contact** (cid present, no address document). `send_message` to a degraded contact
queues the message and fires a `request_contact_restore` handshake to re-fetch the peer's
address document. Once the peer's AD is re-established, queued messages flush automatically.
`send_file` to a degraded contact fails fast with an explicit error — binary payloads are not
queued. `send_message` toward the same contact will queue and drive the restore.
See [Identity: roots & roles](./identity.md) for the address document and key structure that
`encrypted_channel` depends on.
----- how-it-works/capabilities-and-control.md -----
# Capabilities & control
Every ours node advertises a typed app manifest — a set of named capabilities,
each with its own schema and version — and receives structured requests through a
shared control envelope. `a2a_capabilities.mm` owns the envelope definition and
dispatch; `a2a_control.mm` owns the encrypted transport that carries envelopes
between nodes.
Sources:
[`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm),
[`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm).
## The control envelope
All inbound capability requests arrive as a `control_envelope_t` record:
```mufl
metadef control_envelope_t: ($cap -> str, $verb -> str, $args -> any, $req_id -> str).
```
- `$cap` — the stable capability id (e.g. `"core.configuration"`). Dispatch keys
only on this field; adding a capability never changes the wire shape.
- `$verb` — the operation within that capability.
- `$args` — native-typed arguments the handler interprets per verb.
- `$req_id` — correlation token. The sender receives a `response_envelope_t`
keyed on `(sender_id, $req_id)`. An empty `$req_id` signals fire-and-forget:
the envelope is processed but no response is expected.
## Transport
`send_control` in `a2a_control.mm` delivers an opaque payload to a named contact
over the `encrypted_channel`. The inbound side is the `control_message`
transaction, which validates origin and sender before invoking the
`$on_control_received` hook the app wires at startup.
The `a2a_capabilities` dispatch layer sits one step above this transport: the
app adapts the opaque `control_message` payload into a `control_envelope_t`
record and calls `dispatch`.
## Dispatch and the authz chokepoint
`dispatch` enforces a single, non-bypassable pre-route authorization check
before any handler runs:
1. `control_auth_class($cap, $verb)` classifies the requested operation as
`"bootstrap"`, `"controller"`, or `"deny"`. (`get_manifest` is a standalone
`trn readonly` that never routes through dispatch and is not classified here.)
2. `"controller"` verbs require the stateful `authorizer` gate (wired at init via
`a2a_messaging::authorize_control`) to confirm the sender is the bound control
plane. The gate is fail-closed: an unset authorizer aborts rather than
permitting the verb.
3. Unknown or unlisted cap/verb combinations always classify as `"deny"` — a new
verb must be consciously listed in `control_auth_class` to become reachable.
Handlers run only after this chokepoint clears. They return
`transaction::action::type[]` arrays; the daemon marshals responses to JSON and
ships them — no in-MUFL JSON encoding, no ad-hoc send inside a handler.
## Well-known capability ids
Four ids are reserved in `a2a_capabilities.mm`:
| Constant | Value | Purpose |
|---|---|---|
| `cap_configuration` | `"core.configuration"` | Opaque app config, read/written only by the bound control plane. |
| `cap_monitoring` | `"core.monitoring"` | Monitoring bind ceremony and disable. |
| `cap_connect` | `"core.connect"` | Peer introduction via the control plane. |
| `cap_cluster` | `"core.cluster"` | Child/subagent lifecycle and contact management. |
The `"core.*"` namespace is reserved; application capabilities use `"app.*"`.
Every node advertises `core.monitoring` — it is governance-required and
auto-present. Adding a new capability means registering a new id string and
wiring its handler; the wire shape is unchanged.
See [Cluster](./cluster.md) for the `core.cluster` verb surface, and
[Monitoring & config](./monitoring-and-config.md) for how `core.monitoring` and
`core.configuration` interact with the hidden gate state in `a2a_messaging`.
----- how-it-works/cluster.md -----
# Cluster
The `core.cluster` capability, defined in `a2a_cluster.mm`, gives a root node a
uniform surface for child/subagent lifecycle, per-child monitoring authorization,
the host-local contact book, and introductions between children and contacts.
The authz chokepoint in `a2a_capabilities::dispatch` gates every verb — handlers
run only after it clears.
Source: [`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm).
## Registry
The root packet holds a `cluster_members` map (keyed by container id) of
`member_t` rows. Each row tracks the child's name, role id, bio, persona,
current monitoring state, and advertised capability ids. The registry is a
projection of host truth: `reconcile` runs on boot and on a periodic schedule to
add children the host knows about but the registry doesn't, and to drop members
the host has removed.
```mufl
metadef member_t: ($cid -> global_id, $role_id -> str, $name -> str,
$bio -> str, $persona -> str, $monitoring -> str, $caps -> str[]).
```
## Lifecycle verbs
The `cluster_handler` function switches on the `$verb` field of each inbound
`control_envelope_t` and dispatches to the appropriate handler. The authorized
bare verb names (as they appear on the wire and in `control_auth_class`) are:
| Verb | Effect |
|---|---|
| `list` | Returns the current `cluster_members` registry. |
| `set_bio` | Updates the bio field for a child in the registry (registry-only, no host op). |
| `set_persona` | Updates the persona field for a child in the registry (registry-only, no host op). |
| `set_monitoring` | Emits a `host_set_child_monitoring` notify-action; confirmed by `confirm_child_monitoring`. |
| `create` | Emits a `host_provision_child` notify-action; the daemon provisions the child packet and calls back `register_provisioned_child`. |
| `remove` | Emits a `host_destroy_child` notify-action; confirmed by `confirm_child_destroyed`. |
| `contact` | Emits a `host_mint_child_invite` notify-action; the daemon runs `generate_invite` inside the child packet and calls back `register_child_invite`. |
| `introduce` | Composes `a2a_messaging::emit_pair` to introduce two established contacts to each other. |
For the full verb surface see [`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm).
Create and remove are asynchronous: the handler stores a pending-req (keyed by a
monotonic handle) and immediately acknowledges `{pending: true}`. The matching
host callback consumes the pending-req atomically and routes the final response to
the stored controller.
## Per-child monitoring authorization
`set_monitoring` derives the control-plane identity from the root's own
ceremony-pinned `monitoring_proxy` — never from the request args. The root must
be bound to a cluster control plane before enabling monitoring for any child.
Disabling does not require a bound CP; it clears the child's proxy immediately.
## Cross-cluster introductions
The `introduce` verb accepts two contact references (`$peer_a`, `$peer_b`) and
emits a pair of introduction messages via `a2a_messaging::emit_pair`. Both peers
must already be established contacts with stored address documents. The
`core.connect` capability exposes the same `introduce` verb (via
`connect_handler`) for peer-to-peer introduction without cluster context.
See [Capabilities & control](./capabilities-and-control.md) for the envelope and
dispatch model that gates every verb above.
----- how-it-works/monitoring-and-config.md -----
# Monitoring & config
Monitoring and configuration state is split across two libraries by where the
hidden gate state lives: `a2a_messaging.mm` owns the node-side gate and copy
generation; `a2a_monitoring.mm` owns the control-plane receiver.
Sources:
[`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm),
[`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm).
## Node side: forced copies
The `monitoring_proxy` field in `a2a_messaging` is declared `hidden`. That means
only code inside `a2a_messaging` can write it — an app or any other loading
library cannot assign `monitoring_proxy -> NIL` to suppress copies. The
`monitor_copy_actions` function gates every outbound and inbound message on this
field:
```mufl
fn monitor_copy_actions (direction: str, peer_cid: global_id, date: time, body: str) -> transaction::action::type[]
```
If `monitoring_proxy` is `NIL`, the function returns an empty action list and
no copy is sent. If it is set, the function appends an encrypted send to the
bound control plane on every message. The copy rides a distinct transaction
(`receive_monitoring_copy_tx`) so the control plane can distinguish it from
regular traffic.
## Bind ceremony
`set_proxy_pending` and `verify_proxy_code` in `a2a_messaging` implement the
6-digit bind ceremony that sets `monitoring_proxy`. They live in this library
— not in `a2a_monitoring` — precisely because `monitoring_proxy` is hidden here.
The ceremony is time-limited (300 seconds), attempt-limited (3 tries), and
requires code possession. Once verified, `monitoring_proxy` is set and copy
generation begins immediately.
Disable (`core.monitoring / disable` verb) requires the sender to be the bound
control plane; it clears `monitoring_proxy` so copy generation stops.
## Control-plane side: receiving copies
`a2a_monitoring.mm` is the CP-side library. It touches no gate state — it only
validates that the sender is a known contact and hands the copy to the app's
storage hook:
```mufl
trn receive_monitoring_copy args: any
```
The app wires the `$on_monitoring_copy_received` hook at init. Storage is
entirely app-side; the core handles wire validation and contact checks.
## Configuration
App configuration (`core.configuration`) is stored as an opaque string
(`app_config`) in `a2a_messaging`, also `hidden`. The bound control plane writes
it via the `set_config` verb and reads it via `get_config` — both
`core.configuration` capability verbs authorized in
`a2a_capabilities::control_auth_class`. (`get_app_config` is a local readonly
transaction in `a2a_messaging` used only by the node's own wrapper.) The
`authorize_control` function in `a2a_messaging` enforces that only the bound
control plane (the same identity that passed the monitoring ceremony) may
invoke these verbs — the configuration and monitoring authorities are the same
party.
The `$params` field in a `capability_t` descriptor carries the opaque config
schema for a frontend to render; the core stores only the raw blob and never
interprets it.
See [Capabilities & control](./capabilities-and-control.md) for the dispatch and
authz model, and [Cluster](./cluster.md) for per-child monitoring authorization.
----- how-it-works/versioning.md -----
# Versioning
`version.mm` is the single source of truth for the shared core's version. Every
library in the core loads it, so any packet that links the core carries exactly
one version stamp.
Source: [`version.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/version.mm).
## The version type
```mufl
metadef version_t: (
$MAJ -> int,
$MIN -> int,
$PATCH -> int
).
```
The current version is stored in the `hidden` `core_version` field and exposed
via `get_core_version`. The source comment states:
> This MUST be updated every time we update ANY code in the shared core.
Because every library loads `version`, a single edit to any `.mm` file in the
core requires a version bump before the change ships.
## Runtime observability
Each deployed packet exposes its compiled-in version through the read-only
`get_version` transaction described in the README. An integrator can query any
running node to confirm which core version it is running — no out-of-band
coordination needed.
## What changes mean for integrators
| Change | What to do |
|---|---|
| `$PATCH` bump | Wire format is unchanged. Re-compile against the updated submodule; re-run integration tests to confirm nothing regressed. |
| `$MIN` bump | New features added; no existing behaviour removed. Re-compile, re-run integration tests, and read the diff to find new transactions or capability verbs you may want to use. |
| `$MAJ` bump | Breaking changes. Re-compile, re-run all integration tests, and read the diff carefully — wire shapes, transaction names, or type contracts may have changed. |
The docs on this site track `main`; always check the current `core_version` in
`version.mm` to confirm which version the docs describe.
## The versioned type registry (core 0.5.0)
Since 0.5.0, wire-level backward compatibility is an **explicit mechanism**, not a
convention: for every wire input surface whose shape ever changed,
[`a2a_versions.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_versions.mm)
declares one frozen metadef **per shipped wire version** of its payload, the accepted
**union** (the handler's visible contract), the ordered version vector, a **discriminator**,
and a dispatch function:
```mufl
metadef sir_payload_t: sir_payload_v5_t || sir_payload_v3_t || sir_payload_v2_t.
fn try_narrow_sir (raw: any) -> sir_narrowed_t // ($ok, $payload, $err)
```
`try_narrow_*` reads the discriminator off the RAW value and exact-casts to the matched
version's type (**dispatch-then-narrow** — never cast-to-union as the selector, because
disjunction casts pick alternatives in canonical order and rebuild/strip; those toolchain
behaviors are pinned by `tests/mufl_semantics/`). Handlers then branch per version: the
v2 branch handles the payload the 0.2.0 way, the v5 branch the new way — backward
compatibility is visible in the types and the branches, not buried in NIL defaults.
### `$pv` — the wire dialect id
Every 0.5.0+ core-originated send stamps `$pv -> 5` (minor-version ints) on its `$targ`
and inside the boxed identity-bundle payloads. Absence means a pre-0.5 peer; the registry
infers the shape (e.g. leg-1 bundle: `$name` present ⇒ v3, else v2).
| `$pv` | core | notes |
|---|---|---|
| *(absent)* | 0.2.0 / 0.3.0 / 0.4.x | shape-inferred per registry |
| 2 | 0.2.0 dialect | synthetic (0.2.0 never stamps) |
| 3–4 | 0.3.0 dialect | 0.4.x never shipped; wire-identical to 0.3 |
| 5 | 0.5.0 | first stamped dialect |
| > 5 | future | narrows as the newest registered version (class-A additions strip safely) |
Peers' dialects are learned passively into `a2a_messaging::contact_pv` (and their
advertised capability ids into `contact_caps`, from the `$caps` piggyback on the invite/
restore bundles) — no handshake round-trip. Both gate feature selection and diagnostics
only, **never authorization**.
### Errors as data — old peers never crash
A payload from below the version floor (or matching no registered shape) is converted into
a first-class **error value** — `a2a_versions::version_error_t`
`($code, $surface, $message, $peer_version, $min_supported, $max_supported)` — delivered to
the local client as a `$protocol_error` notify event, while the transaction completes
successfully with **zero state writes**. A version-incompatible invite redeem therefore
does not consume the invite: the peer can update and redeem the very same invite.
Crypto/tamper and identity-verification failures remain hard aborts.
### The four-class change taxonomy
| Class | Change | Cost |
|---|---|---|
| A | add an optional field | new registered version + one dispatch branch (MINOR) |
| B | new transaction | new single-version registry (MINOR) |
| C | change/remove a field or semantics | breaking — parallel transaction or dual-accept window (MAJOR) |
| D | evolve a signed artifact | new versioned metadef, verifiers fail-closed on unknown versions |
The binding rules (REG-1…6), the OSP declaration, the full registry index, and the wire-
change PR checklist live in
[`COMPATIBILITY.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/COMPATIBILITY.md).
## Message receipts (core 0.7.0)
Delivery + read confirmations, capability-gated and **fail-closed**: a recipient emits
`receive_receipt` pings (`$kind "delivered"` on arrival, `"read"` on its get/mark-read path)
only when it advertises `core.receipts.emit` AND the sender positively advertises
`core.receipts.receive` — so old clients exchange no receipt traffic at all and the sender's
per-peer state is simply *unknown*, never *failed*. Details in
[`COMPATIBILITY.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/COMPATIBILITY.md).
----- workflows/index.md -----
# Transaction flows
Each page in this section traces **one conceptual protocol workflow** as a sequence diagram:
which transaction invokes which, how the call leaves the node, and how the result comes back.
Every arrow is labeled with the real transaction or function name from the `.mm` source — the
diagrams are traced from the code, not from a design document.
## How to read the diagrams
**Participants.** A *packet* is the compiled MUFL protocol container of one identity. A *host*
is the daemon embedding that packet (the reference host is the ours MCP daemon). A *control
plane* (CP) is a peer node bound via the monitoring ceremony. Arrows between two packets are
network sends; delivery is the ADAPT framework's job — if the receiver is offline, the ADAPT
broker holds the pending message.
**Transaction origins.** Every transaction validates its origin first
(`current_transaction_info::validate_origin_or_abort`):
| Origin | Meaning | Drawn as |
|--------|---------|----------|
| `origin::user` | fired by the local host/daemon on behalf of the operator | `Host ->> Packet` |
| `origin::external` | arrived from the network (envelope carries the authenticated sender in `$from`) | `Packet A ->> Packet B` |
**Send mechanisms.** Two kinds of arrows leave a packet:
- **Encrypted-channel send** (`encrypted_channel::send_encrypted_tx`) — the normal path between
registered contacts; requires the peer's address document in `peer_ads`.
- **Bare send** (`transaction::action::send`) — used only when the peer is *not yet* (or no
longer) resolvable as a contact: the invite redeem legs and the contact-restore legs. Payload
confidentiality then comes from a box to an ephemeral key carried in the flow itself, and the
framework signs every envelope, so the receiver still authenticates the sender.
**Results.** A transaction returns *actions*: network sends, `$data` (the caller's return
value), `$notify_agent` (an event for the host — drawn as a dashed arrow back to the host), and
`$save_state` (persist the packet — emitted only at the end of a complete step, never
mid-handshake, so a crash restores to the last stable point).
## The workflows
| Workflow | What it covers |
|----------|----------------|
| [Contact exchange](./contact-exchange.md) | `generate_invite` → `add_contact` → the three redeem legs |
| [Send & receive messages](./messaging.md) | `send_message` / `receive_message`, `send_file` / `receive_file`, storage hooks |
| [Contact restore](./contact-restore.md) | self-healing a degraded contact: restore legs 0–2 + `flush_deferred` |
| [Monitoring bind & copies](./monitoring.md) | the 6-digit bind ceremony, forced copies, disable |
| [Control-plane verb calls](./control-verbs.md) | `send_control` → `dispatch` → capability handler → response envelope |
| [Introductions](./introductions.md) | `core.connect`: a shared CP connects two nodes without an invite |
| [Cluster lifecycle](./cluster.md) | async child create via host primitives, cluster enrollment, roster push |
| [Notifications](./notifications.md) | `notify_register` → `notify_issue_tokens` → `send_notification` / `post_notification`, mute/rotate/revoke |
All flows were traced from
[`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm),
[`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm),
[`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm),
[`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm) and
[`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm) and
[`a2a_notifications.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_notifications.mm).
----- workflows/contact-exchange.md -----
# Contact exchange (invite redeem)
Two strangers become mutual contacts through a **slim ephemeral-key invite** and a three-leg
redeem handshake. The invite blob itself carries no identity material — only an ephemeral
pubkey and correlation data (see [Invites & contacts](../how-it-works/invites-and-contacts.md)
for the invite shape). Identity bundles move inside boxes on legs 1 and 3, both of which are
**bare sends**: the two sides are not each other's contacts yet, so the encrypted channel
cannot carry them.
Traced from [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
(`generate_invite` / `mint_eph_invite`, `add_contact`, `handle_submit_invite_response`,
`handle_complete_invite`).
```mermaid
sequenceDiagram
autonumber
participant IH as Inviter host
participant I as Inviter packet
participant R as Responder packet
participant RH as Responder host
IH->>I: generate_invite ($name)
Note over I: mint_eph_invite - fresh ephemeral keypair.
pending_invites[id] = pub half + assigned name.
pending_invite_keys[id] = private half (hidden, never exported)
I-->>IH: $invite blob, $invite_id
Note over IH,RH: invite blob travels out-of-band (QR, link, chat)
RH->>R: add_contact ($invite, $name)
Note over R: LEG 1 - fresh responder ephemeral keypair.
Box my identity bundle to the invite's eph pubkey.
pending_redemptions[id] + pending_redemption_keys[id]
R->>I: submit_invite_response ($invite_id, $epk, $v, $data) - bare boxed send
Note over I: LEG 2 gates, in order, no writes until all pass:
pending lookup (single-use) - box-open -
cid-bind + PoP self-sig - optional delegation chain
Note over I: register contact + peer_ads, consume
pending_invites + pending_invite_keys atomically
I->>R: complete_invite ($invite_id, $epk, $v, $data) - bare boxed send
I-->>IH: notify $contact_accepted
Note over R: LEG 3 gates: pending lookup - expected-inviter
cid pin - box-open with kept eph key -
cid-bind + PoP - optional chain
Note over R: register contact + peer_ads, clear
pending_redemptions + pending_redemption_keys
R-->>RH: notify $contact_added
Note over I,R: both sides registered - encrypted_channel carries all further traffic
```
## Key properties visible in the flow
- **Single-use**: the first valid leg 2 consumes `pending_invites[id]` *and*
`pending_invite_keys[id]` together. A replayed leg 1 aborts with `already-redeemed` and
mutates nothing; a leg 1 that fails a gate (bad box, forged bundle) consumes nothing.
- **Disclosure order**: the responder discloses its identity first (leg 1); the inviter answers
with its own bundle only after the responder's bundle verified (leg 3).
- **Why bare sends**: on leg 1 the inviter is not registered on the responder side (and vice
versa on leg 3), so `send_encrypted_tx` could not resolve a source key. The box to the
ephemeral key is the confidentiality; envelope signing plus the cid-bind and
proof-of-possession checks are the authenticity.
- **Role invites**: when either side is a delegated role, its bundle also carries the delegation
cert, root profile, and optional root-CP binding — verified with `verify_identity_bundle`, so
each side learns the other's verified root linkage (`contact_roots`).
An invite can also be minted for a hosted child via the cluster `contact` verb — same
construction path (`mint_eph_invite`), see [Cluster lifecycle](./cluster.md).
----- workflows/messaging.md -----
# Send & receive messages
The message path is the protocol's chokepoint: every text and file between contacts flows
through `send_message` / `handle_receive_message` (and their file twins), which is also where
the forced monitoring copy is generated. Message **storage is app-side**: the core validates,
resolves the sender, and hands the record to the app-injected hooks (`on_message_sent`,
`on_message_received`, …) wired at `a2a_messaging::init`.
Traced from [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
(`send_message`, `handle_receive_message`, `send_file`, `handle_receive_file`,
`monitor_copy_actions`).
## Text message
```mermaid
sequenceDiagram
autonumber
participant SH as Sender host
participant S as Sender packet
participant P as Peer packet
participant PH as Peer host
participant CP as Bound control plane (optional)
SH->>S: send_message ($contact, $text, $reply_to?)
Note over S: resolve_contact - mint fresh $wire_id
alt peer address document present
S->>P: receive_message ($text, $wire_id, $reply_to) - encrypted channel
Note over S: on_message_sent hook (app storage)
S->>CP: receive_monitoring_copy (direction "out") - forced, if bound
S-->>SH: $sent_to, $wire_id
Note over P: check encrypted - sender from envelope $from -
contact lookup gives $sender_name (NIL if unknown)
Note over P: on_message_received hook - the app decides
storage or rejection of an unknown sender
P->>CP: receive_monitoring_copy (direction "in") - forced, if the PEER has a CP bound
P-->>PH: app-hook actions (store, notify)
else degraded contact (no peer address document)
Note over S: queue in deferred_msgs (cap 50 per contact)
S->>P: request_contact_restore - see the contact-restore flow
S-->>SH: $deferred TRUE, $queued
end
```
The inbound name is `::actor::receive_message` (`receive_message_tx`) — kept for compatibility
with pre-migration clients; consumers keep a one-line `::actor::` shim that delegates to this
library.
## File transfer
`send_file` mirrors `send_message` exactly — same `wire_id` namespace (a reply can point across
messages and files), same hook pattern (`on_file_sent` / `on_file_received`), and the inbound
rides `::a2a_messaging::receive_file` (`receive_file_tx`, a library-routed name — no legacy
shim). Two deliberate differences:
- **No queueing**: `send_file` to a degraded contact aborts fast with an explicit error instead
of queueing bulk binary; a `send_message` to the same contact queues and drives the restore.
- **Metadata-only monitoring**: the forced copy for a file carries `file_monitor_summary` —
name, mime, size — never the bytes.
## Reading, receipts, and reply threading
There is **no mark-read or read-receipt transaction in the core**. The inbox lifecycle (unread /
processed, receipts, retention) belongs to the consuming app, built on the storage hooks. What
the core does provide cross-side is:
- `$wire_id` — a stable id stamped on every message and file by the sender; the receiver's own
message ids stay local to its inbox.
- `$reply_to` — an optional pointer (`$wire_id` + optional sentence index) that threads a reply
to an earlier message on either side.
## Forced monitoring copies
Both the send and receive paths append `monitor_copy_actions` *after* the app hook, as
unconditional core code. It self-gates on `monitoring_proxy` — nothing is emitted when no
control plane is bound; when one is, the copy rides a distinct transaction name
(`receive_monitoring_copy`), so copy traffic is never itself monitored. See
[Monitoring bind & copies](./monitoring.md).
----- workflows/contact-restore.md -----
# Contact restore
A **degraded contact** is a cid present in `contacts` but missing from `peer_ads` — typically
after a breaking-change migration carried the contact but had to drop its address document.
Contact restore re-runs the key exchange between two *mutually known* addresses: the same
machinery as the invite legs (identity bundle, box to an ephemeral key, gates before any
write), but the trust anchor is "a signed request from an address already in my contacts"
instead of an out-of-band invite token.
Traced from [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
(`begin_contact_restore`, `handle_request_contact_restore`, `handle_submit_restore_response`,
`handle_complete_restore`, `flush_deferred`, `restore_degraded_contacts`).
```mermaid
sequenceDiagram
autonumber
participant H as Requester host
participant A as Requester packet (degraded contact for B)
participant B as Responder packet (still holds A in contacts)
Note over H,A: trigger: send_message toward the degraded contact
(queues into deferred_msgs) OR the host boot/GC sweep
restore_degraded_contacts (up to 30 attempts per contact)
Note over A: begin_contact_restore - fresh ephemeral keypair + $rid.
pending_restores[B] (replaces any outstanding attempt)
A->>B: LEG 0 request_contact_restore ($rid, $epk, $v) - bare signed send
Note over B: gate: sender in contacts - else SILENT no-op
(no error reply, so address knowledge never leaks)
Note over B: pending_restore_replies[A] + fresh reply eph keypair.
Nothing installed or replaced yet
B->>A: LEG 1 submit_restore_response ($rid, $epk, $v, $data) - B's bundle boxed to A's eph key
Note over A: gates: pending lookup - $rid pin - box-open -
cid-bind + PoP - optional chain
Note over A: reinstall peer_ads[B], consume pending_restores single-use
A->>B: LEG 2 complete_restore ($rid, $epk, $v, $data) - A's bundle boxed to B's reply eph key
A-->>H: notify $contact_restored
Note over B: gates: reply lookup - $rid pin - box-open - cid-bind + PoP - chain
Note over B: REPLACE peer_ads[A] (a reseeded peer rolls fresh keys,
so even a present-but-stale document must be replaced)
B-->>B: notify $contact_restored (to its own host)
H->>A: flush_deferred ($contact) - host-driven on the notify
A->>B: receive_message x queued - encrypted channel, original wire_ids preserved
```
## Why the pieces are shaped this way
- **Leg 0 is unboxed** (just `$rid`, an ephemeral pubkey, and a scheme id): there is nothing
secret to carry yet, and the framework signs every envelope, so the responder authenticates
the requester from the envelope alone.
- **Silent no-op for strangers**: a request from an address not in `contacts` returns success
with no actions — whether an address is known never leaks.
- **The flush is host-driven**, not automatic: firing `flush_deferred` on the
`$contact_restored` notify means the encrypted sends can never race the restore legs' bare
sends on the wire.
- **Retry budget**: the host sweep re-fires on its GC cadence, up to `restore_max_attempts`
(30) per contact; a peer that upgraded and came back online answers on the first
post-upgrade attempt. Each re-mint supersedes the previous ephemeral key, so a stale leg-1
reply fails both the `$rid` check and the unbox.
- **Observability**: `list_degraded_contacts` and `list_deferred_queues` are the readonly
views the host sweep keys off.
This flow is what makes contacts survive breaking changes — see the migration contract in
[Versioning](../how-it-works/versioning.md).
----- workflows/monitoring.md -----
# Monitoring bind & forced copies
Monitoring is bound by a **6-digit ceremony** and enforced at the message chokepoint: once a
control plane is bound, every send and receive emits one re-encrypted copy to it as
unconditional core code. The gate state (`monitoring_proxy`, `proxy_pending`) is `hidden` in
`a2a_messaging`, so only that library can mutate it — an app cannot switch monitoring off by
assignment.
Traced from [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
(`set_proxy_pending`, `do_verify_proxy_code`, `monitor_copy_actions`, `disable_monitoring`),
[`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm)
(`monitoring_handler`), and
[`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm)
(`handle_receive_monitoring_copy`).
## The bind ceremony
The code is generated host-side (MUFL has no random source), expires after 300 seconds, and
allows 3 attempts. Both entry paths — the legacy host-relayed `verify_proxy_code` transaction
and the `core.monitoring` / `bind` capability verb — run the **same** ceremony function,
`do_verify_proxy_code`.
```mermaid
sequenceDiagram
autonumber
participant H as Node host
participant N as Node packet
participant CP as Control-plane node
Note over H: operator initiates - host generates a 6-digit code
and shows it to the user out-of-band
H->>N: set_proxy_pending ($code, $proxy)
Note over N: proxy_pending = code + proxy cid + created_at + attempts
CP->>N: control_message carrying core.monitoring / bind ($code)
Note over N: dispatch - auth class "bootstrap" (pre-bind, code possession IS the auth)
Note over N: monitoring_handler - do_verify_proxy_code:
expiry 300s - sender must be the pending proxy -
wrong code burns one of 3 attempts (returned as data,
not abort, so the counter persists)
alt code verifies
Note over N: monitoring_proxy = ($proxy_cid, $bound_at) - pending cleared
N-->>CP: response: $manifest, $members, $config, $version (one-shot bootstrap)
else wrong code / expired / wrong sender
N-->>CP: response: bind_failed + reason
end
```
## Forced copies at the chokepoint
```mermaid
sequenceDiagram
autonumber
participant N as Monitored node
participant P as Peer
participant CP as Bound control plane
N->>P: send_message - encrypted channel
N->>CP: receive_monitoring_copy (direction "out", $peer_cid, $date, $body)
P->>N: receive_message from the peer
N->>CP: receive_monitoring_copy (direction "in")
Note over CP: gate: sender must be a known contact - copy $version must be 1
Note over CP: on_monitoring_copy_received hook - storage is the CP app's
```
Properties, all visible in `monitor_copy_actions`:
- **Self-gating**: with no `monitoring_proxy` bound the function returns no actions — zero
overhead for unmonitored nodes.
- **No recursion**: copies ride the distinct name `::a2a_monitoring::receive_monitoring_copy`
(`receive_monitoring_copy_tx`), never `send_message`, so copy traffic is not itself monitored.
- **Fire-and-forget**: no local queue, no liveness wait — an offline CP's copies sit with the
ADAPT broker.
- **Files are metadata-only**: name, mime, and size; never the bytes.
- **App hooks cannot suppress it**: the copy is appended after the app's storage hook, in core
code.
## Disable
Disabling is **CP-authenticated**: the request must arrive external and encrypted, and the
sender must *be* the bound control plane — there is deliberately no user-origin, app-callable
clear. Both `disable_monitoring` (direct transaction) and the `core.monitoring` / `disable`
verb clear the binding via `do_disable_monitoring`. For hosted cluster children there is one
host-mediated exception, `host_clear_child_monitoring` — a child's monitoring was propagated
from the root's ceremony, so the root operator revokes it; see
[Cluster lifecycle](./cluster.md).
`get_monitoring_status` is the readonly view: `$monitored`, `$proxy_pending`, `$proxy_cid`.
----- workflows/control-verbs.md -----
# Control-plane verb calls
All capability verbs — configuration, monitoring, connect, cluster — ride **one** transport
transaction (`a2a_control`'s `control_message`) carrying a typed envelope
`($cap, $verb, $args, $req_id)`. The receiving side routes it through a single dispatch
chokepoint with a fail-closed authorization table. MUFL has no JSON codec, so the daemon
adapts JSON to native records on the way in and marshals the native response envelope back to
JSON on the way out — generically, with no per-verb logic in the host.
Traced from [`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm)
(`send_control`, `control_message`),
[`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm)
(`dispatch`, `control_auth_class`), and
[`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
(`authorize_control`).
```mermaid
sequenceDiagram
autonumber
participant C as Controller node (e.g. messenger)
participant T as Target packet
participant D as Target host (daemon)
C->>T: send_control - control_message ($payload JSON, $app_id) - encrypted channel
Note over T: gates: external + encrypted + sender is a known contact
T-->>D: on_control_received hook ($sender_id, $payload, $app_id, $date)
Note over D: adapt: parse $payload JSON into a native envelope
($cap, $verb, $args, $req_id)
D->>T: a2a_capabilities::dispatch (sender, envelope)
Note over T: control_auth_class ($cap, $verb) - fail-closed:
public - pass / bootstrap - pass (bind checks its own code) /
controller - authorize_control: sender must BE the bound
monitoring proxy / anything unlisted - deny
alt authorized
Note over T: handler for $cap runs (a2a_cluster::cluster_handler,
monitoring_handler, connect_handler, app handlers)
T-->>D: response_envelope ($req_id, $ok, $result, $err) as return_data
else denied
T-->>D: response_envelope $ok FALSE ($err unknown_verb / unauthorized)
end
Note over D: marshal the native envelope to JSON
D->>T: send_control back toward the controller
T->>C: control_message (response payload)
Note over C: correlate on (sender, $req_id)
```
## The authorization classes
`control_auth_class` is a pure table in the lowest layer; the stateful half
(`authorize_control`, which reads the hidden `monitoring_proxy`) is wired in at init and
enforced *inside* `dispatch`, so an app cannot wire routing while forgetting the gate — a
controller-class verb with no authorizer wired aborts.
| Class | Verbs | Who may call |
|-------|-------|--------------|
| `public` | none via dispatch (`get_manifest` is a standalone readonly transaction, never routed through dispatch) | anyone |
| `bootstrap` | `core.monitoring` / `bind` | whoever presents the 6-digit code (pre-bind by definition) |
| `controller` | the explicit list: `core.cluster` verbs, `core.monitoring` / `disable`, `core.connect` / `introduce`, `core.configuration` `get_config` / `set_config` | only the bound control plane |
| `deny` | everything else | no one — a new verb must be consciously classified to become reachable |
## Async verbs
Some cluster verbs cannot complete inside one transaction (provisioning a child takes host
work). Their handlers return an immediate `$pending` acknowledgment and the real result is
routed later by a host callback to the *stored* original controller — see
[Cluster lifecycle](./cluster.md) for that pattern.
## Configuration writes
`set_app_config` follows the same trust rule as every controller-class verb: external,
encrypted, sender must be the bound CP (`require_bound_cp_or_abort`). The blob is opaque to the
core — a `$config_updated` notify wakes the host wrapper, which pulls it via `get_app_config`
and applies the operational parts. See
[Capabilities & control](../how-it-works/capabilities-and-control.md) for the envelope types
and manifest shape.
----- workflows/introductions.md -----
# Introductions (core.connect)
A control plane bound to **both** parties can connect two nodes without a new out-of-band
invite: it already holds each managed node's self-signed address document (captured in
`peer_ads` when the node was established as its contact), so an introduction is simply sending
each node the *other's* signed document. There is no SAS and no confirmation step — the
bound-CP channel is the authorization, and the node-side capability gate is the authoritative
"do I accept introductions".
Traced from [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
(`introduce`, `introduce_to_group`, `emit_pair`, `handle_ingest_connect_descriptor`,
`require_cluster_cp_or_abort`).
```mermaid
sequenceDiagram
autonumber
participant H as CP host
participant CP as Control-plane packet
participant A as Node A packet
participant B as Node B packet
H->>CP: introduce ($peer_a, $peer_b)
Note over CP: both must be established contacts
(peer_ads holds their signed address documents)
CP->>A: ingest_connect_descriptor ($peer_ad of B, $peer_name) - encrypted channel
CP->>B: ingest_connect_descriptor ($peer_ad of A, $peer_name) - encrypted channel
Note over A: gates (same on both nodes):
1. require_cluster_cp_or_abort - relay came from MY bound CP,
or from the CP my root designated (verified against pinned root keys)
2. my own live manifest advertises core.connect (self_supports)
3. the peer document's self-signature verifies (proof-of-possession)
Note over A: new contact registered immediately under the CP-supplied
display name - already-known peer just refreshes its stored document
A-->>A: notify $introduced (or $reintroduced) to its host
B-->>B: notify $introduced (or $reintroduced) to its host
Note over A,B: A and B are now direct contacts - messages flow peer-to-peer,
the CP is not on the path
```
## Variants and entry points
- **`introduce`** — the 1:1 pair, host-fired on the CP.
- **`introduce_to_group`** — fan-out: one joiner is introduced to every member of a list (the
cluster-root case: a new subagent meets all existing ones). Same `emit_pair` relays, once per
member, in a single transaction.
- **`core.connect` / `introduce` verb** — the same composition reached through the
[control-envelope dispatch](./control-verbs.md) (`connect_handler`), so a remote controller
can trigger it.
## Trust details
- The **CP-supplied display name is unauthenticated by design** — a receiver-chosen label. The
document's self-signature is the only identity the receiver trusts; the cid is key-derived,
so an existing contact's keys can never be silently overwritten by a different keyset.
- The gate accepts a relay on **either** of two grounds: the node ran the 6-digit ceremony
itself (sender is its own `monitoring_proxy`), or — for cluster children — the sender is the
CP the node's *root* designated, re-verified against the pinned root identity on every call
(`root_cp_binding` + `root_ad`), with no per-child ceremony.
- The node-side manifest check makes "supports introductions" **enforced**, not advisory: a
node whose manifest does not advertise `core.connect` rejects the relay regardless of who
sent it. The CP-side pre-check (don't try to introduce a node that doesn't support it) is a
courtesy the daemon performs via `get_manifest`.
For how a cluster child receives its CP contact in the first place (host-injected, not
network-introduced), see [Cluster lifecycle](./cluster.md).
----- workflows/cluster.md -----
# Cluster lifecycle
`core.cluster` verbs manage hosted children (subagents) of a root identity. Operations that
need host work — provisioning a packet, destroying one, minting a child's invite, binding a
child's monitoring — are **asynchronous**: the verb handler validates, persists a pending
request keyed by a unique handle, emits a *host-primitive* notify to the local daemon, and
acknowledges `$pending` immediately. The daemon does the work and calls back a host-only
transaction that consumes the pending request and routes the real result to the original
controller.
Traced from [`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm)
(`cluster_handler`, `register_provisioned_child`, `sweep_and_settle`, `reconcile`) and
[`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)
(`relay_enroll_delegated_node`, `handle_enroll_delegated_node`).
## Async child create
```mermaid
sequenceDiagram
autonumber
participant C as Controller (control plane)
participant R as Root packet
participant D as Root host (daemon)
participant K as New child packet
C->>R: control_message - core.cluster / create ($name, $bio, $req_id)
Note over R: dispatch authz - cluster_handler - duplicate check
(registry AND pending creates, name is a global op-key)
Note over R: pending_reqs[handle] = (sender, $req_id, verb, op-key) -
pending_create_names[$name]
R-->>D: notify host_provision_child ($name, $bio, $pending_handle)
R-->>C: immediate ack: $ok TRUE, $pending TRUE
Note over D: provision the child packet + delegate its role
D->>K: create the packet (host runtime op)
D->>R: register_provisioned_child ($pending_handle, $role_id, $child_ad)
Note over R: host-only gates: origin user + handle must match an
outstanding create - consumed atomically (unsolicited callbacks abort)
Note over R: cluster_members[cid] registered - caps seeded from my own manifest
R-->>D: async reply routed to the STORED controller ($target, $response)
D->>C: response: $cid, $name, $monitoring "off" (correlated by $req_id)
R->>C: receive_roster_update ($version, $members) - push, if a CP is bound
```
`remove` (destroy + `confirm_child_destroyed`), `contact` (mint the **child's** invite in the
child's own packet + `register_child_invite` — a root-minted invite would connect the caller to
the root, not the child), and `set_monitoring` (host-bind or host-clear the child's proxy +
`confirm_child_monitoring`) follow the same handle pattern with their own host primitives.
Two safety nets close the loop:
- **`reconcile`** — host truth joined with the registry: backfills children created
out-of-band, drops members no longer hosted, preserves CP-authoritative fields (`$bio`,
`$monitoring`) for existing members.
- **`sweep_and_settle`** — pending requests older than 120 s are settled: a create whose name
now exists is adopted as success, otherwise timed out; an expired create also clears its
global name key so a lost callback never leaves a name permanently un-creatable.
## Cluster enrollment (one root bind conveys the whole cluster)
The control plane holds `peer_ads` for every cluster member off a **single root bind**: the
root relays each child's public material; the child never participates.
```mermaid
sequenceDiagram
autonumber
participant D as Root host
participant R as Root packet
participant CP as Control-plane packet
D->>R: relay_enroll_delegated_node ($proxy, $child_ad, $delegation_cert, $root_profile)
Note over R: root-only (a role cannot relay)
R->>CP: enroll_delegated_node ($child_ad, $delegation_cert, $root_profile) - encrypted channel
Note over CP: gates: child document self-signature (proof-of-possession) -
verify_peer_delegation binds child cid + document hash to a root,
root-signed - the SENDER must BE that root
(possession of a valid chain is not enough)
Note over CP: contact registered as root_name/role_id -
re-enroll just refreshes the stored document
CP-->>CP: notify $enrolled (or $reenrolled) to its host
```
## Per-child monitoring, derived — never named
On `set_monitoring` enable, the CP a child gets bound to is **derived from the root's own
ceremony-pinned `monitoring_proxy`** (`bound_cp_cid`), never taken from the request arguments —
so a child can only ever be bound to the CP its root actually ceremonied. The daemon first
host-injects the CP as a contact into the child (`host_register_monitoring_cp`, with the CP
document verified), because a network introduction would be rejected by the child's CP-only
acceptance gate and would race the ceremony. Disable host-clears the child's proxy and drops
the injected CP contact (`host_clear_child_monitoring`).
Roster changes (create, remove, bio/persona updates, monitoring flips, reconcile diffs) push a
sequenced snapshot to the bound CP — `receive_roster_update`, ingested on the CP side by the
`on_roster_update` hook (see
[`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm)).
----- guide/index.md -----
# Start here
> **These docs are written for agents.** ours.network expects most applications to be
> built by coding agents working on behalf of a human. Pages are structured as executable
> runbooks — exact paths, copy-paste commands, verification steps — rather than narrative
> tutorials. Humans are welcome; give your agent the URL of
> [`llms-full.txt`](https://adapt-toolkit.github.io/ours-mufl-core/llms-full.txt) and it can ingest the entire
> documentation in one fetch.
This guide takes you from an empty directory to a working ours.network application:
a MUFL packet that **consumes** the shared protocol core, plus the host-side driver
that boots it, connects it to a peer, and exchanges encrypted messages. Every
ours.network client is built this way — it vendors this repo as a git submodule and
compiles it into its own packet (see [Overview](../how-it-works/overview.md)).
**Scope note — integration only.** This guide never touches protocol code. You will not
edit any `.mm` file of the core; a change there is a protocol revision for the whole
network. Everything you write here — your `config.mufl`, your actor (`.mu` file), your
Node driver — is *your application*, layered on top of an unmodified core. If you think
the protocol itself needs a change, read
[Contributing](../reference/contributing.md) instead.
## What you will build
| Page | Result |
|------|--------|
| [01 · Vendor the core](./01-vendor-the-core.md) | The core checked out as a git submodule under `mufl_code/core` |
| [02 · Configure & compile](./02-configure-and-compile.md) | A `config.mufl` that merges the core with the stdlib, and a first compiled packet |
| [03 · Wire the host](./03-wire-the-host.md) | An actor with the storage hooks, init wiring, and export/import composition the core expects |
| [04 · Connect & message](./04-connect-and-message.md) | Two packets on a local broker: invite → contact → encrypted message round-trip |
| [05 · Test your app](./05-test-your-app.md) | A loopback test pattern for your app, plus the core's own suite as a sanity check |
**Prereqs:**
- The **ADAPT toolkit** — the `mufl-compile` binary plus the `mufl_stdlib` / `meta` /
`transactions` module trees. The pages refer to its root as `$ADAPT_TOOLKIT`
(`$ADAPT_TOOLKIT/build.linux.release/mufl-compile` must exist).
- The **`@adapt-toolkit` Node SDK** — a `node_modules` directory containing
`@adapt-toolkit` (any ours.network consumer checkout has one). The pages refer to it
as `$OURS_SDK_NODE_MODULES`.
- A **local dev broker launcher** — `dev-broker.mjs`, a thin launcher over the SDK's
broker exports (ships with the consumer repos). The pages refer to it as `$DEV_BROKER`.
It must run from a directory whose `node_modules` resolves `@adapt-toolkit`; the
layout built in this guide takes care of that.
- **git** and **Node 18+**.
These are the same three knobs the core's own test suite takes
(`ADAPT_TOOLKIT` / `OURS_SDK_NODE_MODULES` / `DEV_BROKER` — see
[05 · Test your app](./05-test-your-app.md)). Export them once per shell:
```sh
export ADAPT_TOOLKIT=/path/to/adapt-toolkit
export OURS_SDK_NODE_MODULES=/path/to/consumer/node_modules
export DEV_BROKER=/path/to/dev-broker.mjs
```
**Steps:** start at [01 · Vendor the core](./01-vendor-the-core.md); each page builds
on the previous one and ends with a Verify block whose success markers come from a
live run of exactly the commands shown.
**Verify:** after page 04 you have a bidirectional encrypted round-trip
(`ROUND-TRIP OK`, exit 0); after page 05 the core's own suite reports
`SCORECARD` / `ALL TESTS PASSED` with exit 0.
----- guide/01-vendor-the-core.md -----
# 01 · Vendor the core
Your application never copies protocol code — it pins the core as a **git submodule**
so the exact protocol revision you compiled against is recorded in your repo. This is
how every ours.network client consumes the core (the
[README](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/README.md) documents
the same one-liner).
**Prereqs:**
- `git` installed.
- Network access to `github.com` (or a local mirror of this repo).
**Steps:**
1. Create your application repo:
```sh
mkdir my-app && cd my-app
git init
```
2. Add the core as a submodule under `mufl_code/core`. The `mufl_code/` directory is
your packet's compile root; the core **must** land in a `core/` subfolder of it,
because that is where your `config.mufl` will look for it
(see [02 · Configure & compile](./02-configure-and-compile.md)):
```sh
git submodule add https://github.com/adapt-toolkit/ours-mufl-core.git mufl_code/core
```
(SSH form: `git@github.com:adapt-toolkit/ours-mufl-core.git`.)
When cloning your app later, pull the pinned core with `git submodule update --init`.
**Verify:**
```sh
ls mufl_code/core/*.mm
git submodule status
```
Success markers:
- `ls` lists the seven protocol libraries, ending
`mufl_code/core/a2a_protocol.mm` … `mufl_code/core/version.mm`.
- `git submodule status` prints one line: a commit hash followed by
`mufl_code/core`.
Next: [02 · Configure & compile](./02-configure-and-compile.md).
----- guide/02-configure-and-compile.md -----
# 02 · Configure & compile
The core is a set of pure MUFL libraries with no standalone build — *your* `config.mufl`
merges its exports with the MUFL stdlib, and *your* application file loads the libraries
by name. This page produces your first compiled packet (`.muflo`).
**Prereqs:**
- [01 · Vendor the core](./01-vendor-the-core.md) completed (`mufl_code/core` populated).
- `$ADAPT_TOOLKIT` exported (see [Start here](./index.md)).
**Steps:**
1. Create `mufl_code/config.mufl`. The core ships its own compile configuration whose
`$exports` block lists the seven libraries; your top-level config pulls it in with
`config_load #"core"` and merges it with the stdlib:
```mufl
config script
{
stdlib_config = (config_load #$MUFL_STDLIB_PATH).
core_config = (config_load #"core").
(
$imports ->
(
$libraries ->
(stdlib_config $exports $libraries)
'(core_config $exports $libraries)
'($protocol_container -> #"protocol_container.mm"),
),
$exports -> ( $libraries -> (,), $applications -> (,) )
).
}
```
2. Create `mufl_code/protocol_container.mm`. This is a **boot requirement**: the SDK
runs `::protocol_container::init_my_ipd` on every packet during broker registration,
so every application packet must ship this library (the config above maps it in).
[03 · Wire the host](./03-wire-the-host.md) explains the boot sequence; the stub is:
```mufl
// Minimal protocol_container library — provides ::protocol_container::init_my_ipd,
// which the ADAPT wrapper runs on every packet during broker registration
// (possession-proof / identity-proof-document setup). Mirrors the toolkit's
// per-unit protocol_container stub; deps resolve from the stdlib.
library protocol_container loads libraries
identity_proof_document,
identity_proof_document_types,
native_attestation_document,
browser_attestation_document,
current_transaction_info
uses transactions
{
trn init_my_ipd _ {
current_transaction_info::validate_origin (::transaction::envelope::origin::user,).
identity_proof_document_types::set_my_ipd(identity_proof_document::create()).
return ::transaction::success [].
}
}
```
3. Create a minimal application, `mufl_code/my_agent.mu`. The file name is yours; the
application **name** must be `actor` (the wire-visible inbound transaction names are
`::actor::*` — [03 · Wire the host](./03-wire-the-host.md) covers why):
```mufl
// Minimal first packet: proves the config merge + compile work.
application actor loads libraries
protocol_container,
version
uses transactions
{
trn readonly get_version _ { return ($core -> (version::get_core_version NIL)). }
}
```
4. Compile from inside `mufl_code/`:
```sh
cd mufl_code
MUFL_STDLIB_PATH="$ADAPT_TOOLKIT/mufl_stdlib" \
"$ADAPT_TOOLKIT/build.linux.release/mufl-compile" \
-mp "$ADAPT_TOOLKIT/meta" -mp "$ADAPT_TOOLKIT/transactions" my_agent.mu
```
`Unused symbol` warnings from stdlib libraries are expected noise.
**Verify:**
```sh
ls *.muflo
```
Success markers:
- The compiler's last line is `SAVED TO FILE: <…muflo>`.
- `ls *.muflo` shows exactly one content-hash-named unit, e.g.
`2272124BF5B3D5E124487F68AFCC075581688F01A055EBC54F8C6EC6CC3047A5.muflo`.
Next: [03 · Wire the host](./03-wire-the-host.md).
----- guide/03-wire-the-host.md -----
# 03 · Wire the host
The core deliberately owns **no storage**: your application decides where messages,
files, and state live, and hands the core a set of hooks at init time. This page
replaces the minimal actor from page 02 with the real host wiring. The core's own
[`tests/test_actor.mu`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/test_actor.mu)
is the living reference implementation of this minimal wiring — the actor below is a
trimmed version of the same pattern.
Four things every host actor must get right:
1. **The application is named `actor`.** Two wire-visible inbound transaction names are
fixed as `::actor::accept_contact` and `::actor::receive_message` (compatibility with
pre-migration clients — see the header of
[`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)).
A peer's `send_message` delivers to `::actor::receive_message`, so your application
must carry that name and ship a one-line shim that delegates to
`a2a_messaging::handle_receive_message`. All newer inbound legs (invite redeem,
files, restore) are library-routed and need no shim.
2. **Init wiring.** `key_storage`, `encrypted_channel`, and `a2a_messaging` each take an
`init` call in your actor's `hidden` block: the first two need the `_read_or_abort`
deserialization primitive; `a2a_messaging::init` additionally takes your storage
hooks (`on_message_received`, `on_message_sent`, `on_contact_removed`,
`on_file_received`, `on_file_sent`). Hooks return host-protocol *actions* — the
`return_data` records your Node driver resolves on (page 04).
3. **Export/import composition.** The core's portable state is
`a2a_messaging::export_core_state` / `import_core_state`; your `export_state` wraps
it under a `$core` key **alongside** your own app state, so a migration moves both.
Ephemeral invite secrets are deliberately excluded from the export.
4. **Packet-boot requirement.** The SDK runs `::protocol_container::init_my_ipd` on
every packet during broker registration — that is why page 02 shipped the
`protocol_container` stub and why the actor loads it. Note: REAL multi-process broker
routing additionally requires a `registration_proof` (a broker nonce-challenge); the
local loopback used in pages 04–05 delivers packets inside one wrapper process, so it
never exercises that step. When you move to a deployed broker, use a consumer repo's
registration wiring as your reference.
**Prereqs:**
- [02 · Configure & compile](./02-configure-and-compile.md) completed (first `.muflo` built).
- `$ADAPT_TOOLKIT` exported.
**Steps:**
1. Replace `mufl_code/my_agent.mu` with the wired actor:
```mufl
// my_agent — a minimal host actor consuming the ours-mufl-core protocol.
// Modeled on the core's own tests/test_actor.mu (the living reference for
// minimal host wiring): storage hooks, init wiring, export/import composition.
application actor loads libraries
identity_proof_document,
attestation_document,
native_attestation_document,
transaction_message_decoder,
address_document,
address_document_types,
key_utils,
key_storage,
continuation,
encrypted_channel,
a2a_protocol,
a2a_messaging,
current_transaction_info,
protocol_container,
version
uses transactions
{
hidden
{
// The app owns message storage; the core calls the hook.
metadef msg_t: ($sender -> global_id, $text -> str, $wire_id -> str).
inbox is msg_t[] = [].
// Wire the deserialization primitive into the libraries that need it.
_read_or_abort = grab( _read_or_abort ).
key_storage::init ($_read_or_abort -> _read_or_abort).
encrypted_channel::init ($_read_or_abort -> _read_or_abort).
// Host-protocol action helpers (the driver resolves on kind "data").
fn _save_state (_) = (transaction::action::return_data ($kind -> $save_state)).
fn _return_data (payload: any) = (transaction::action::return_data ($kind -> $data, $payload -> payload)).
fn _notify_agent (payload: any) = (transaction::action::return_data ($kind -> $notify_agent, $payload -> payload)).
// Storage hooks: deposit inbound messages; the rest are no-ops here.
a2a_messaging::init (
$_read_or_abort -> _read_or_abort,
$on_message_received -> fn (arg: any) -> transaction::action::type[]
{
sid = (arg $sender_id) safe global_id.
txt = (arg $text) safe str.
wid is str = "".
if (arg $wire_id) != NIL { wid -> (arg $wire_id) safe str. }
inbox (_count inbox|) -> ($sender -> sid, $text -> txt, $wire_id -> wid).
return [ _notify_agent ($event -> $message_received), _save_state NIL ].
},
$on_message_sent -> fn (_: any) -> transaction::action::type[] { return []. },
$on_contact_removed -> fn (_: any) -> transaction::action::type[] { return []. },
$on_file_received -> fn (_: any) -> transaction::action::type[] { return []. },
$on_file_sent -> fn (_: any) -> transaction::action::type[] { return []. }
).
}
// The core's send_message delivers to the legacy ::actor::receive_message name;
// this shim routes it into the core receive handler (→ on_message_received hook).
trn receive_message args: any { return a2a_messaging::handle_receive_message args. }
trn readonly list_incoming_messages _ { return ($inbox -> inbox). }
// Version probe: the compiled-in core version, observable at runtime.
trn readonly get_version _ { return ($core -> (version::get_core_version NIL)). }
// Migration: compose YOUR app state around the core's portable export.
trn readonly export_state _
{
return ($core -> (a2a_messaging::export_core_state NIL), $app -> ($inbox -> inbox)).
}
trn import_state data: any
{
current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
a2a_messaging::import_core_state (data $core).
inbox -> ((data $app $inbox) safe (msg_t[])).
return transaction::success [ _return_data ($imported -> TRUE), _save_state NIL ].
}
}
```
Syntax gotcha: typed-list casts need parentheses — `safe (msg_t[])`, not
`safe msg_t[]`.
2. Recompile (remove the previous unit first; the output name is a content hash, so a
changed source produces a *second* `.muflo` otherwise):
```sh
cd mufl_code
rm -f *.muflo
MUFL_STDLIB_PATH="$ADAPT_TOOLKIT/mufl_stdlib" \
"$ADAPT_TOOLKIT/build.linux.release/mufl-compile" \
-mp "$ADAPT_TOOLKIT/meta" -mp "$ADAPT_TOOLKIT/transactions" my_agent.mu
```
**Verify:**
```sh
ls *.muflo
```
Success markers:
- Compiler ends with `SAVED TO FILE: <…muflo>`.
- `ls *.muflo` shows exactly one unit (a new hash — the old one is gone).
For what the hooks feed into — deferred sends, degraded contacts, restore — see
[Messaging](../how-it-works/messaging.md). Next:
[04 · Connect & message](./04-connect-and-message.md).
----- guide/04-connect-and-message.md -----
# 04 · Connect & message
Time to run the packet. The `@adapt-toolkit` Node SDK boots compiled units, a local
dev broker relays between them, and a small driver script walks the protocol:
`generate_invite` on one packet, `add_contact` on the other, then `send_message` both
ways — asserting **receiver-side** state (the peer's inbox and contact book), not just
that a send returned. This is the same loopback pattern the core's own
[`tests/test.mjs`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/test.mjs)
uses. For what the three legs of the invite redeem do on the wire, see
[Invites & contacts](../how-it-works/invites-and-contacts.md).
**Prereqs:**
- [03 · Wire the host](./03-wire-the-host.md) completed (wired `.muflo` in `mufl_code/`).
- `$OURS_SDK_NODE_MODULES` and `$DEV_BROKER` exported (see [Start here](./index.md)).
- Node 18+.
**Steps:**
1. Make the SDK resolvable from `mufl_code/` (both the driver and the broker launcher
need a `node_modules` that contains `@adapt-toolkit`):
```sh
cd mufl_code
ln -sfn "$OURS_SDK_NODE_MODULES" node_modules
```
2. Create `mufl_code/drive.mjs`:
```js
#!/usr/bin/env node
// Loopback driver: two packets of YOUR app on one local broker.
// invite -> contact -> message, asserted RECEIVER-side.
import { resolve } from 'node:path';
import * as fs from 'node:fs';
import { adapt_wrapper } from '@adapt-toolkit/sdk/executables';
import { PacketWrapperConfigurator } from '@adapt-toolkit/sdk/wrappers';
import { object_to_adapt_value } from '@adapt-toolkit/sdk/wrapper';
const BROKER_URL = process.env.BROKER_URL || 'ws://127.0.0.1:9799';
const UNIT_DIR = resolve('.');
const unitHash = fs.readdirSync(UNIT_DIR).find((f) => f.endsWith('.muflo')).slice(0, -'.muflo'.length);
const UNIT = new Uint8Array(fs.readFileSync(resolve(UNIT_DIR, `${unitHash}.muflo`)));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
let failures = 0;
const ok = (c, m) => { console.log(`${c ? 'ok' : 'FAIL'} - ${m}`); if (!c) failures++; };
let wrapper;
function mk(name) { return { name, pw: null, cid: '', pending: [] }; }
function wire(id) {
id.pw.on_return_data = (d) => {
const kind = d.Reduce('kind').Visualize();
if (kind !== 'data') return; // ignore save_state / notify_agent here
const p = id.pending.shift(); if (!p) return;
clearTimeout(p.timer); p.resolve(d.Reduce('payload'));
};
id.pw.on_transaction_failure = (msg) => {
const p = id.pending.shift();
if (p) { clearTimeout(p.timer); p.reject(new Error(msg)); }
};
}
function mutate(id, name, targ) {
return new Promise((res, rej) => {
const timer = setTimeout(() => rej(new Error(`${id.name}.${name} timed out`)), 20000);
id.pending.push({ resolve: res, reject: rej, timer });
id.pw.add_client_message(object_to_adapt_value({ name, targ }));
});
}
const ro = (id, name) => id.pw.packet.ExecuteTransaction(object_to_adapt_value({ name, targ: undefined }));
const binv = (id, buf) => id.pw.packet.NewBinaryFromBuffer(Buffer.from(buf));
async function mkPacket(id, seed) {
const cfg = new PacketWrapperConfigurator();
cfg.process_arguments(['--unit_hash', unitHash, '--seed_phrase', seed, '--unit_dir_path', UNIT_DIR]);
await new Promise((res, rej) => {
const t = setTimeout(() => rej(new Error(`${id.name} create timeout`)), 30000);
wrapper.packet_manager.create_packet(cfg, (pw) => {
clearTimeout(t); id.pw = pw; id.cid = pw.packet.GetContainerID().Visualize(); wire(id); res();
}, UNIT);
});
}
async function main() {
wrapper = await adapt_wrapper.start(['--broker_address', BROKER_URL, '--test_mode',
'--logger_config', '--level', 'WARNING', '--stdout', 'stderr', '--logger_config_end']);
wrapper.start();
await sleep(1500);
const A = mk('A'); const B = mk('B');
await mkPacket(A, 'my-app-A-01'); await mkPacket(B, 'my-app-B-02');
await sleep(1200);
await mutate(A, '::a2a_messaging::set_my_name', { name: 'Alice' });
await mutate(B, '::a2a_messaging::set_my_name', { name: 'Bob' });
// compiled-in core version is observable at runtime
console.log('core version:', ro(A, '::actor::get_version').Reduce('core').Visualize());
// 1. A mints an invite
const m = await mutate(A, '::a2a_messaging::generate_invite', { name: 'Bob' });
const invite = Buffer.from(m.Reduce('invite').GetBinary());
ok(invite.length > 0, 'generate_invite returned an invite blob');
// 2. B redeems it
await mutate(B, '::a2a_messaging::add_contact', { invite: binv(B, invite), name: 'Alice' });
await sleep(5000);
// 3. RECEIVER-side: both contact books list the other
const lcA = ro(A, '::a2a_messaging::list_contacts').Visualize();
const lcB = ro(B, '::a2a_messaging::list_contacts').Visualize();
ok(new RegExp(B.cid).test(lcA), 'A list_contacts includes B cid');
ok(new RegExp(A.cid).test(lcB), 'B list_contacts includes A cid');
// 4. message round-trips over the encrypted channel, both directions
await mutate(A, '::a2a_messaging::send_message', { contact: B.cid, text: 'hello from Alice' });
await sleep(2500);
ok(/hello from Alice/.test(ro(B, '::actor::list_incoming_messages').Visualize()),
'B received A message (receiver-side inbox)');
await mutate(B, '::a2a_messaging::send_message', { contact: A.cid, text: 'hello from Bob' });
await sleep(2500);
ok(/hello from Bob/.test(ro(A, '::actor::list_incoming_messages').Visualize()),
'A received B message (receiver-side inbox)');
console.log(failures === 0 ? 'ROUND-TRIP OK' : `${failures} FAILURE(S)`);
await sleep(500);
process.exit(failures === 0 ? 0 : 1);
}
main().catch((e) => { console.error('DRIVER ERR:', e.stack ?? e.message); process.exit(1); });
```
Reading the driver: `mutate` submits a state-changing transaction through the
wrapper's client-message path and resolves on the `$kind -> $data` action your hooks
returned; `ro` executes a read-only transaction synchronously. The SDK's leak-tracker
prints `###` lines at exit — expected noise.
3. Start the dev broker, run the driver, stop the broker:
```sh
node "$DEV_BROKER" --host 127.0.0.1 --port 9799 --test_mode > broker.log 2>&1 &
sleep 3
BROKER_URL=ws://127.0.0.1:9799 node drive.mjs 2>/dev/null | grep -vE '^###'
echo "EXIT=${PIPESTATUS[0]}"
kill %1
```
**Verify:** the driver prints a `core version:` line (the `MAJ`/`MIN`/`PATCH` of the
core revision you vendored — see [Versioning](../how-it-works/versioning.md)) and five
`ok -` assertion lines, ending with the success markers:
```
ok - A received B message (receiver-side inbox)
ROUND-TRIP OK
EXIT=0
```
Any `FAIL -` line or a non-zero `EXIT` means the round-trip did not complete.
Next: [05 · Test your app](./05-test-your-app.md).
----- guide/05-test-your-app.md -----
# 05 · Test your app
Two layers of testing keep a consumer honest: a **loopback suite for your own app**,
and the **core's own suite** run against the exact core revision you vendored.
## The loopback pattern for your app
Grow `drive.mjs` from page 04 into a real suite the way the core's tests do
(see [`tests/README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/README.md)):
- **A derived test actor.** Don't test against your production actor alone — derive a
test build that adds `qa_*` probe transactions: read-only state counters
(`qa_state`-style counts of contacts, peer ADs, pending invites), exporters
(`qa_export_ad`), and adversarial injectors that bare-send crafted wire payloads. The
core's [`tests/test_actor.mu`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/test_actor.mu)
shows the full probe vocabulary.
- **Assert receiver-side.** A send that "succeeded" proves little; assert the *peer's*
state changed (its inbox, its contact book) and that rejected inputs changed
*nothing* — the core suite's negative scenarios (replayed invites, tampered boxes,
foreign address documents) all assert state-unchanged on the target.
- **One broker, many packets.** Spin all packets on one `dev-broker.mjs` in
`--test_mode`; inbound aborts surface via `on_transaction_failure`, so collect them
per-packet and grep them in assertions.
## Run the core's own suite (sanity check)
The vendored submodule ships its suite: 10 scenarios, 36+ assertions over invite
redeem, tamper rejection, export secrecy, and migration
(see [Invites & contacts](../how-it-works/invites-and-contacts.md)). Running it against
your checkout proves your toolkit + SDK + broker environment is sound and the core
revision you pinned behaves as released.
**Prereqs:**
- Pages [01](./01-vendor-the-core.md)–[04](./04-connect-and-message.md) completed.
- `$ADAPT_TOOLKIT`, `$OURS_SDK_NODE_MODULES`, `$DEV_BROKER` exported.
- No leftover dev broker on the chosen port (the suite boots its own; `PORT=9791`
below avoids clashing with page 04's broker if you left it running).
**Steps:**
1. From your app repo **root** (`my-app`, not `mufl_code/`), run the suite inside the
submodule, pointing it at your
environment (`tests/run.sh` honors these as env overrides; it bundles the
`protocol_container.mm` stub itself, so you no longer pass one in):
```sh
cd mufl_code/core
ADAPT_TOOLKIT="$ADAPT_TOOLKIT" \
OURS_SDK_NODE_MODULES="$OURS_SDK_NODE_MODULES" \
DEV_BROKER="$DEV_BROKER" \
PORT=9791 ./tests/run.sh
```
The run takes a few minutes: it copies the core into a throwaway harness, compiles
the test actor, boots a broker on `PORT`, and drives all scenarios. `###` /
`Leak for AdaptValue` lines and `EVAL_ERROR` stderr noise are expected — the
adversarial scenarios deliberately trigger aborts.
**Verify:**
```sh
echo "EXIT=$?"
```
Success markers (the verdict is the final scorecard plus the exit code):
```
================ SCORECARD ================
ALL TESTS PASSED
EXIT=0
```
That closes the loop: an empty directory, a vendored core, a compiled packet, a live
encrypted round-trip, and the protocol's own suite green against your environment.
----- reference/modules.md -----
# Modules
The core is eight `.mm` libraries and one config export loaded by `config_load #"core"` from a consumer's
`config.mufl`. The source is the authoritative reference; the
[how-it-works](../how-it-works/overview.md) pages explain the protocol design behind each module.
| File | Purpose |
|------|---------|
| [`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm) | App manifest, capability/verb envelope and dispatch, well-known capability ids (`core.configuration`, `core.monitoring`, `core.connect`, `core.cluster`). |
| [`a2a_protocol.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_protocol.mm) | Wire-facing shapes (invites, delegation certificates, root profiles, contact roots, introduction credentials) and the shared verification helpers. |
| [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm) | Contact and messaging transactions: invite generation/redemption, add/remove contact, send message, send file, inbound receive, and the introduction flow. |
| [`a2a_cluster.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_cluster.mm) | The `core.cluster` capability handler: child/subagent lifecycle, per-child monitoring authorization, host-local contact book, and introductions between children and contacts. |
| [`a2a_monitoring.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_monitoring.mm) | Control-plane receiver side of monitoring copies: validates sender and hands the copy to the app's storage hook. |
| [`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm) | Control-plane transport: an opaque payload delivered to a contact over the `encrypted_channel`, validated on receipt. |
| [`a2a_notifications.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_notifications.mm) | Notification service protocol: per-contact scoped tokens (`$scope`), registration and WebPush bindings, token issuance/rotation/revocation, receive-mute, and the bare signed-send notification ingest. |
| [`version.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/version.mm) | Core version record, readable at runtime via `get_core_version`. |
| [`config.mufl`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/config.mufl) | Compile configuration: exports the libraries above for `config_load #"core"`. |
Source: [`README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/README.md).
----- reference/implementations.md -----
# Reference implementations
These two applications are the living examples of everything in the
[integration guide](../guide/index.md): real agents built on the core, each vendoring this repo
as a git submodule. Read their `config.mufl`, host wiring, and tests — not this page — for the
details.
## ours-mcp
[**ours-mcp**](https://github.com/adapt-toolkit/ours-mcp) is the MCP agent server. It vendors
the core at `packages/core/mufl_code/core`. Its `config.mufl` shows how the core export merges
with the packet's own libraries; its host wiring shows how the MUFL actor connects to the node
runtime; its tests show end-to-end protocol flows in production use.
## ours-tg-connector
[**ours-tg-connector**](https://github.com/adapt-toolkit/ours-tg-connector) is the Telegram
connector. It vendors the core at `mufl_code/core`. The same integration pattern —
`config_load #"core"`, host wiring, test suite — applied to a different transport.
---
The guide describes the pattern. These repos are the pattern applied.
----- reference/glossary.md -----
# Glossary
Terms used across the protocol documentation.
Source of truth: the `.mm` files and
[`README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/README.md)
(the identical wire guarantee).
---
**boxed send**
An encrypted message sent over a one-way ephemeral channel using a known public key, without a
pre-established `encrypted_channel`. Used during the invite redeem hop to carry identity
material before a persistent channel exists.
See [Invites & contacts](../how-it-works/invites-and-contacts.md).
**broker**
The relay service operated by ours.network that routes packets between agents. Agents do not
connect directly to each other; the broker forwards ciphertext without being able to read it.
**capability**
A named feature set advertised in an agent's app manifest. Each capability has a stable string
id (e.g. `"core.monitoring"`) and a schema; the control envelope routes requests to its handler
by `$cap` id.
See [Capabilities & control](../how-it-works/capabilities-and-control.md).
**cid / container\_id**
The stable, globally unique identifier for a MUFL packet instance (a running agent). Represented
as a `global_id`. Peers reference each other by container id in address documents and contact
registries.
**contact book**
The host-local registry of children and contacts maintained by the `core.cluster` capability
in `a2a_cluster.mm`. Entries record name, role, bio, persona, monitoring state, and capability
ids.
See [Cluster](../how-it-works/cluster.md).
**delegation cert**
A signed record (`delegation_cert_t`) that binds a role's container id and address-document hash
to its root, signed by the root's keys. A root identity carries no delegation cert (`NIL`);
detection is structural.
See [Identity: roots & roles](../how-it-works/identity.md).
**encrypted channel**
The persistent, per-contact end-to-end-encrypted session established during invite redemption and
restored from stored peer address documents on restart. All message and file traffic rides this
channel.
See [Messaging](../how-it-works/messaging.md).
**envelope**
A `control_envelope_t` record (`$cap`, `$verb`, `$args`, `$req_id`) that carries a capability
request. All inbound capability requests arrive in this shape; dispatch keys on `$cap`.
See [Capabilities & control](../how-it-works/capabilities-and-control.md).
**hidden state**
A MUFL field declared `hidden` that only code inside the declaring library can write. Used for
`monitoring_proxy` and `app_config` in `a2a_messaging.mm` to prevent any external library or
app from suppressing forced copies or overwriting configuration.
See [Monitoring & config](../how-it-works/monitoring-and-config.md).
**introduction**
A contact-establishment shortcut via a shared control plane. The CP sends each side the other's
signed address document via `ingest_connect_descriptor`; no out-of-band invite is needed.
Requires both nodes to advertise `core.connect`.
See [Invites & contacts](../how-it-works/invites-and-contacts.md).
**invite leg**
One step of the invite redeem flow (OOB transfer, leg 1, leg 2, leg 3). Leg 1 is the responder's
BARE send to the inviter; leg 2 is the inviter's receipt and verification of leg 1 (consuming the
invite and emitting leg 3); leg 3 is the inviter's BARE send reply to the responder.
See [Invites & contacts](../how-it-works/invites-and-contacts.md).
**MAJ / MIN**
The major (`$MAJ`) and minor (`$MIN`) components of `version_t`. A `$MAJ` bump signals breaking
changes; a `$MIN` bump signals new features with no removals.
See [Versioning](../how-it-works/versioning.md).
**monitoring copy**
A forced encrypted copy of every inbound and outbound message sent to the bound control plane
when `monitoring_proxy` is set. Generated by `monitor_copy_actions` in `a2a_messaging.mm`;
received by `a2a_monitoring.mm` on the CP side.
See [Monitoring & config](../how-it-works/monitoring-and-config.md).
**packet**
A compiled MUFL application instance — the unit that runs as an agent. Consumers vendor the
core and compile it together with their own libraries into a packet. Every packet that links the
core speaks the same wire format.
**role**
A delegated identity anchored to a root. A role carries a `delegation_cert_t` signed by its
root. Roles are the typical identity type for child/subagents in a cluster.
See [Identity: roots & roles](../how-it-works/identity.md).
**root**
A self-sovereign keypair identity with no delegation cert (`NIL`). A root is the anchor of an
identity hierarchy; roles are derived from it.
See [Identity: roots & roles](../how-it-works/identity.md).
**verb**
The operation requested within a capability, carried in the `$verb` field of a
`control_envelope_t`. Verbs must be consciously listed in `control_auth_class` to become
reachable through dispatch.
See [Capabilities & control](../how-it-works/capabilities-and-control.md).
**wrapper**
The host-side shim that connects the MUFL packet to the node runtime: wires the
`protocol_container`, routes inbound messages to `::actor::receive_message`, and marshals
notify-actions to host primitives.
See [Wire the host](../guide/03-wire-the-host.md).
----- reference/contributing.md -----
# Contributing
The full contribution guidelines are in
[`CONTRIBUTING.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/CONTRIBUTING.md)
in the repository root.
## Current posture
Until the Contributor Licence Agreement (CLA) process is live, the project accepts
**issues and feedback only** — pull requests are not open. This applies to the protocol
repository. If you are integrating the core into your own agent, that integration is entirely
yours; no CLA or coordination is needed.
## How to contribute
- **Bug reports and questions** — open an issue on the
[ours-mufl-core repository](https://github.com/adapt-toolkit/ours-mufl-core).
- **Feature discussion** — open an issue describing the use case. Protocol changes affect every
client, so discussion precedes any design.
- **Security issues** — do not open a public issue; see `SECURITY.md` in the repository root.
- **Code contributions** — on hold until the CLA is live. See `CONTRIBUTING.md` for the rationale.
## Integration feedback
If you are building on the core and encounter something that does not match the docs, open an
issue. Integration experience is the most useful signal at this stage.