----- 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.