@ours.network/notifications 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +231 -0
  3. package/dist/cli.js +446 -0
  4. package/dist/config.js +104 -0
  5. package/dist/daemon.js +353 -0
  6. package/dist/http.js +31 -0
  7. package/dist/log-store.js +48 -0
  8. package/dist/presence.js +157 -0
  9. package/dist/provision.js +29 -0
  10. package/dist/push.js +6 -0
  11. package/dist/web-push-sender.js +41 -0
  12. package/mufl_code/20E39399689579D5AFA5132C59DEDDD328C3BCA1A74D546F2193B36585DDD283.muflo +0 -0
  13. package/mufl_code/config.mufl +21 -0
  14. package/mufl_code/core/.github/FUNDING.yml +3 -0
  15. package/mufl_code/core/.github/workflows/docs.yml +39 -0
  16. package/mufl_code/core/.github/workflows/tests.yml +66 -0
  17. package/mufl_code/core/COMMERCIAL-LICENCE.md +25 -0
  18. package/mufl_code/core/COMPATIBILITY.md +221 -0
  19. package/mufl_code/core/CONTRIBUTING.md +37 -0
  20. package/mufl_code/core/LICENSE +98 -0
  21. package/mufl_code/core/NOTIFICATIONS_CONTRACT.md +290 -0
  22. package/mufl_code/core/README.md +91 -0
  23. package/mufl_code/core/SECURITY.md +40 -0
  24. package/mufl_code/core/TRADEMARKS.md +24 -0
  25. package/mufl_code/core/a2a_capabilities.mm +449 -0
  26. package/mufl_code/core/a2a_cluster.mm +601 -0
  27. package/mufl_code/core/a2a_control.mm +90 -0
  28. package/mufl_code/core/a2a_messaging.mm +4817 -0
  29. package/mufl_code/core/a2a_monitoring.mm +94 -0
  30. package/mufl_code/core/a2a_notification_integration.mm +144 -0
  31. package/mufl_code/core/a2a_notifications.mm +1127 -0
  32. package/mufl_code/core/a2a_protocol.mm +246 -0
  33. package/mufl_code/core/a2a_versions.mm +801 -0
  34. package/mufl_code/core/config.mufl +29 -0
  35. package/mufl_code/core/docs/.vitepress/config.mts +58 -0
  36. package/mufl_code/core/docs/citations.json +688 -0
  37. package/mufl_code/core/docs/guide/01-vendor-the-core.md +50 -0
  38. package/mufl_code/core/docs/guide/02-configure-and-compile.md +100 -0
  39. package/mufl_code/core/docs/guide/03-wire-the-host.md +154 -0
  40. package/mufl_code/core/docs/guide/04-connect-and-message.md +156 -0
  41. package/mufl_code/core/docs/guide/05-test-your-app.md +75 -0
  42. package/mufl_code/core/docs/guide/index.md +63 -0
  43. package/mufl_code/core/docs/how-it-works/capabilities-and-control.md +77 -0
  44. package/mufl_code/core/docs/how-it-works/cluster.md +65 -0
  45. package/mufl_code/core/docs/how-it-works/identity.md +56 -0
  46. package/mufl_code/core/docs/how-it-works/invites-and-contacts.md +76 -0
  47. package/mufl_code/core/docs/how-it-works/messaging.md +55 -0
  48. package/mufl_code/core/docs/how-it-works/monitoring-and-config.md +72 -0
  49. package/mufl_code/core/docs/how-it-works/overview.md +45 -0
  50. package/mufl_code/core/docs/how-it-works/versioning.md +116 -0
  51. package/mufl_code/core/docs/index.md +27 -0
  52. package/mufl_code/core/docs/package-lock.json +3467 -0
  53. package/mufl_code/core/docs/package.json +15 -0
  54. package/mufl_code/core/docs/reference/contributing.md +26 -0
  55. package/mufl_code/core/docs/reference/glossary.md +108 -0
  56. package/mufl_code/core/docs/reference/implementations.md +23 -0
  57. package/mufl_code/core/docs/reference/modules.md +19 -0
  58. package/mufl_code/core/docs/scripts/check-citations.mjs +36 -0
  59. package/mufl_code/core/docs/scripts/check-denylist.mjs +30 -0
  60. package/mufl_code/core/docs/scripts/gen-llms.mjs +32 -0
  61. package/mufl_code/core/docs/workflows/cluster.md +89 -0
  62. package/mufl_code/core/docs/workflows/contact-exchange.md +60 -0
  63. package/mufl_code/core/docs/workflows/contact-restore.md +60 -0
  64. package/mufl_code/core/docs/workflows/control-verbs.md +70 -0
  65. package/mufl_code/core/docs/workflows/index.md +57 -0
  66. package/mufl_code/core/docs/workflows/introductions.md +60 -0
  67. package/mufl_code/core/docs/workflows/messaging.md +75 -0
  68. package/mufl_code/core/docs/workflows/monitoring.md +84 -0
  69. package/mufl_code/core/docs/workflows/notifications.md +109 -0
  70. package/mufl_code/core/release-notes/0.1.md +60 -0
  71. package/mufl_code/core/release-notes/0.3.md +22 -0
  72. package/mufl_code/core/release-notes/0.4.md +97 -0
  73. package/mufl_code/core/tests/README.md +83 -0
  74. package/mufl_code/core/tests/compat/README.md +82 -0
  75. package/mufl_code/core/tests/compat/compat.mjs +245 -0
  76. package/mufl_code/core/tests/compat/compat_peer.mjs +146 -0
  77. package/mufl_code/core/tests/compat/run_compat.sh +96 -0
  78. package/mufl_code/core/tests/corpus.mjs +139 -0
  79. package/mufl_code/core/tests/corpus_actor.mu +242 -0
  80. package/mufl_code/core/tests/fatcompile_actor.mu +52 -0
  81. package/mufl_code/core/tests/mig.mjs +805 -0
  82. package/mufl_code/core/tests/mig_actor.mu +518 -0
  83. package/mufl_code/core/tests/migapp.mjs +431 -0
  84. package/mufl_code/core/tests/migapp_actor.mu +294 -0
  85. package/mufl_code/core/tests/mufl_semantics/run.sh +69 -0
  86. package/mufl_code/core/tests/mufl_semantics/t11_three_version.mufl +47 -0
  87. package/mufl_code/core/tests/mufl_semantics/t2_extra_fields.mufl +11 -0
  88. package/mufl_code/core/tests/mufl_semantics/t7_runtime.mufl +30 -0
  89. package/mufl_code/core/tests/mufl_semantics/t9_passthrough.mufl +19 -0
  90. package/mufl_code/core/tests/mufl_semantics/t_str_order.mufl +19 -0
  91. package/mufl_code/core/tests/notif.mjs +755 -0
  92. package/mufl_code/core/tests/notif_actor.mu +450 -0
  93. package/mufl_code/core/tests/protocol_container.mm +18 -0
  94. package/mufl_code/core/tests/run.sh +65 -0
  95. package/mufl_code/core/tests/run_corpus.sh +51 -0
  96. package/mufl_code/core/tests/run_fatcompile.sh +40 -0
  97. package/mufl_code/core/tests/run_mig.sh +51 -0
  98. package/mufl_code/core/tests/run_migapp.sh +51 -0
  99. package/mufl_code/core/tests/run_notif.sh +57 -0
  100. package/mufl_code/core/tests/test.mjs +750 -0
  101. package/mufl_code/core/tests/test_actor.mu +564 -0
  102. package/mufl_code/core/tests/test_common.mjs +93 -0
  103. package/mufl_code/core/version.mm +24 -0
  104. package/mufl_code/packet.mu +170 -0
  105. package/mufl_code/protocol_container.mm +18 -0
  106. package/package.json +43 -0
  107. package/scripts/gen-vapid.mjs +16 -0
@@ -0,0 +1,50 @@
1
+ # 01 · Vendor the core
2
+
3
+ Your application never copies protocol code — it pins the core as a **git submodule**
4
+ so the exact protocol revision you compiled against is recorded in your repo. This is
5
+ how every ours.network client consumes the core (the
6
+ [README](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/README.md) documents
7
+ the same one-liner).
8
+
9
+ **Prereqs:**
10
+
11
+ - `git` installed.
12
+ - Network access to `github.com` (or a local mirror of this repo).
13
+
14
+ **Steps:**
15
+
16
+ 1. Create your application repo:
17
+
18
+ ```sh
19
+ mkdir my-app && cd my-app
20
+ git init
21
+ ```
22
+
23
+ 2. Add the core as a submodule under `mufl_code/core`. The `mufl_code/` directory is
24
+ your packet's compile root; the core **must** land in a `core/` subfolder of it,
25
+ because that is where your `config.mufl` will look for it
26
+ (see [02 · Configure & compile](./02-configure-and-compile.md)):
27
+
28
+ ```sh
29
+ git submodule add https://github.com/adapt-toolkit/ours-mufl-core.git mufl_code/core
30
+ ```
31
+
32
+ (SSH form: `git@github.com:adapt-toolkit/ours-mufl-core.git`.)
33
+
34
+ When cloning your app later, pull the pinned core with `git submodule update --init`.
35
+
36
+ **Verify:**
37
+
38
+ ```sh
39
+ ls mufl_code/core/*.mm
40
+ git submodule status
41
+ ```
42
+
43
+ Success markers:
44
+
45
+ - `ls` lists the seven protocol libraries, ending
46
+ `mufl_code/core/a2a_protocol.mm` … `mufl_code/core/version.mm`.
47
+ - `git submodule status` prints one line: a commit hash followed by
48
+ `mufl_code/core`.
49
+
50
+ Next: [02 · Configure & compile](./02-configure-and-compile.md).
@@ -0,0 +1,100 @@
1
+ # 02 · Configure & compile
2
+
3
+ The core is a set of pure MUFL libraries with no standalone build — *your* `config.mufl`
4
+ merges its exports with the MUFL stdlib, and *your* application file loads the libraries
5
+ by name. This page produces your first compiled packet (`.muflo`).
6
+
7
+ **Prereqs:**
8
+
9
+ - [01 · Vendor the core](./01-vendor-the-core.md) completed (`mufl_code/core` populated).
10
+ - `$ADAPT_TOOLKIT` exported (see [Start here](./index.md)).
11
+
12
+ **Steps:**
13
+
14
+ 1. Create `mufl_code/config.mufl`. The core ships its own compile configuration whose
15
+ `$exports` block lists the seven libraries; your top-level config pulls it in with
16
+ `config_load #"core"` and merges it with the stdlib:
17
+
18
+ ```mufl
19
+ config script
20
+ {
21
+ stdlib_config = (config_load #$MUFL_STDLIB_PATH).
22
+ core_config = (config_load #"core").
23
+ (
24
+ $imports ->
25
+ (
26
+ $libraries ->
27
+ (stdlib_config $exports $libraries)
28
+ '(core_config $exports $libraries)
29
+ '($protocol_container -> #"protocol_container.mm"),
30
+ ),
31
+ $exports -> ( $libraries -> (,), $applications -> (,) )
32
+ ).
33
+ }
34
+ ```
35
+
36
+ 2. Create `mufl_code/protocol_container.mm`. This is a **boot requirement**: the SDK
37
+ runs `::protocol_container::init_my_ipd` on every packet during broker registration,
38
+ so every application packet must ship this library (the config above maps it in).
39
+ [03 · Wire the host](./03-wire-the-host.md) explains the boot sequence; the stub is:
40
+
41
+ ```mufl
42
+ // Minimal protocol_container library — provides ::protocol_container::init_my_ipd,
43
+ // which the ADAPT wrapper runs on every packet during broker registration
44
+ // (possession-proof / identity-proof-document setup). Mirrors the toolkit's
45
+ // per-unit protocol_container stub; deps resolve from the stdlib.
46
+ library protocol_container loads libraries
47
+ identity_proof_document,
48
+ identity_proof_document_types,
49
+ native_attestation_document,
50
+ browser_attestation_document,
51
+ current_transaction_info
52
+ uses transactions
53
+ {
54
+ trn init_my_ipd _ {
55
+ current_transaction_info::validate_origin (::transaction::envelope::origin::user,).
56
+ identity_proof_document_types::set_my_ipd(identity_proof_document::create()).
57
+ return ::transaction::success [].
58
+ }
59
+ }
60
+ ```
61
+
62
+ 3. Create a minimal application, `mufl_code/my_agent.mu`. The file name is yours; the
63
+ application **name** must be `actor` (the wire-visible inbound transaction names are
64
+ `::actor::*` — [03 · Wire the host](./03-wire-the-host.md) covers why):
65
+
66
+ ```mufl
67
+ // Minimal first packet: proves the config merge + compile work.
68
+ application actor loads libraries
69
+ protocol_container,
70
+ version
71
+ uses transactions
72
+ {
73
+ trn readonly get_version _ { return ($core -> (version::get_core_version NIL)). }
74
+ }
75
+ ```
76
+
77
+ 4. Compile from inside `mufl_code/`:
78
+
79
+ ```sh
80
+ cd mufl_code
81
+ MUFL_STDLIB_PATH="$ADAPT_TOOLKIT/mufl_stdlib" \
82
+ "$ADAPT_TOOLKIT/build.linux.release/mufl-compile" \
83
+ -mp "$ADAPT_TOOLKIT/meta" -mp "$ADAPT_TOOLKIT/transactions" my_agent.mu
84
+ ```
85
+
86
+ `Unused symbol` warnings from stdlib libraries are expected noise.
87
+
88
+ **Verify:**
89
+
90
+ ```sh
91
+ ls *.muflo
92
+ ```
93
+
94
+ Success markers:
95
+
96
+ - The compiler's last line is `SAVED TO FILE: <…muflo>`.
97
+ - `ls *.muflo` shows exactly one content-hash-named unit, e.g.
98
+ `2272124BF5B3D5E124487F68AFCC075581688F01A055EBC54F8C6EC6CC3047A5.muflo`.
99
+
100
+ Next: [03 · Wire the host](./03-wire-the-host.md).
@@ -0,0 +1,154 @@
1
+ # 03 · Wire the host
2
+
3
+ The core deliberately owns **no storage**: your application decides where messages,
4
+ files, and state live, and hands the core a set of hooks at init time. This page
5
+ replaces the minimal actor from page 02 with the real host wiring. The core's own
6
+ [`tests/test_actor.mu`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/test_actor.mu)
7
+ is the living reference implementation of this minimal wiring — the actor below is a
8
+ trimmed version of the same pattern.
9
+
10
+ Four things every host actor must get right:
11
+
12
+ 1. **The application is named `actor`.** Two wire-visible inbound transaction names are
13
+ fixed as `::actor::accept_contact` and `::actor::receive_message` (compatibility with
14
+ pre-migration clients — see the header of
15
+ [`a2a_messaging.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_messaging.mm)).
16
+ A peer's `send_message` delivers to `::actor::receive_message`, so your application
17
+ must carry that name and ship a one-line shim that delegates to
18
+ `a2a_messaging::handle_receive_message`. All newer inbound legs (invite redeem,
19
+ files, restore) are library-routed and need no shim.
20
+ 2. **Init wiring.** `key_storage`, `encrypted_channel`, and `a2a_messaging` each take an
21
+ `init` call in your actor's `hidden` block: the first two need the `_read_or_abort`
22
+ deserialization primitive; `a2a_messaging::init` additionally takes your storage
23
+ hooks (`on_message_received`, `on_message_sent`, `on_contact_removed`,
24
+ `on_file_received`, `on_file_sent`). Hooks return host-protocol *actions* — the
25
+ `return_data` records your Node driver resolves on (page 04).
26
+ 3. **Export/import composition.** The core's portable state is
27
+ `a2a_messaging::export_core_state` / `import_core_state`; your `export_state` wraps
28
+ it under a `$core` key **alongside** your own app state, so a migration moves both.
29
+ Ephemeral invite secrets are deliberately excluded from the export.
30
+ 4. **Packet-boot requirement.** The SDK runs `::protocol_container::init_my_ipd` on
31
+ every packet during broker registration — that is why page 02 shipped the
32
+ `protocol_container` stub and why the actor loads it. Note: REAL multi-process broker
33
+ routing additionally requires a `registration_proof` (a broker nonce-challenge); the
34
+ local loopback used in pages 04–05 delivers packets inside one wrapper process, so it
35
+ never exercises that step. When you move to a deployed broker, use a consumer repo's
36
+ registration wiring as your reference.
37
+
38
+ **Prereqs:**
39
+
40
+ - [02 · Configure & compile](./02-configure-and-compile.md) completed (first `.muflo` built).
41
+ - `$ADAPT_TOOLKIT` exported.
42
+
43
+ **Steps:**
44
+
45
+ 1. Replace `mufl_code/my_agent.mu` with the wired actor:
46
+
47
+ ```mufl
48
+ // my_agent — a minimal host actor consuming the ours-mufl-core protocol.
49
+ // Modeled on the core's own tests/test_actor.mu (the living reference for
50
+ // minimal host wiring): storage hooks, init wiring, export/import composition.
51
+ application actor loads libraries
52
+ identity_proof_document,
53
+ attestation_document,
54
+ native_attestation_document,
55
+ transaction_message_decoder,
56
+ address_document,
57
+ address_document_types,
58
+ key_utils,
59
+ key_storage,
60
+ continuation,
61
+ encrypted_channel,
62
+ a2a_protocol,
63
+ a2a_messaging,
64
+ current_transaction_info,
65
+ protocol_container,
66
+ version
67
+ uses transactions
68
+ {
69
+ hidden
70
+ {
71
+ // The app owns message storage; the core calls the hook.
72
+ metadef msg_t: ($sender -> global_id, $text -> str, $wire_id -> str).
73
+ inbox is msg_t[] = [].
74
+
75
+ // Wire the deserialization primitive into the libraries that need it.
76
+ _read_or_abort = grab( _read_or_abort ).
77
+ key_storage::init ($_read_or_abort -> _read_or_abort).
78
+ encrypted_channel::init ($_read_or_abort -> _read_or_abort).
79
+
80
+ // Host-protocol action helpers (the driver resolves on kind "data").
81
+ fn _save_state (_) = (transaction::action::return_data ($kind -> $save_state)).
82
+ fn _return_data (payload: any) = (transaction::action::return_data ($kind -> $data, $payload -> payload)).
83
+ fn _notify_agent (payload: any) = (transaction::action::return_data ($kind -> $notify_agent, $payload -> payload)).
84
+
85
+ // Storage hooks: deposit inbound messages; the rest are no-ops here.
86
+ a2a_messaging::init (
87
+ $_read_or_abort -> _read_or_abort,
88
+ $on_message_received -> fn (arg: any) -> transaction::action::type[]
89
+ {
90
+ sid = (arg $sender_id) safe global_id.
91
+ txt = (arg $text) safe str.
92
+ wid is str = "".
93
+ if (arg $wire_id) != NIL { wid -> (arg $wire_id) safe str. }
94
+ inbox (_count inbox|) -> ($sender -> sid, $text -> txt, $wire_id -> wid).
95
+ return [ _notify_agent ($event -> $message_received), _save_state NIL ].
96
+ },
97
+ $on_message_sent -> fn (_: any) -> transaction::action::type[] { return []. },
98
+ $on_contact_removed -> fn (_: any) -> transaction::action::type[] { return []. },
99
+ $on_file_received -> fn (_: any) -> transaction::action::type[] { return []. },
100
+ $on_file_sent -> fn (_: any) -> transaction::action::type[] { return []. }
101
+ ).
102
+ }
103
+
104
+ // The core's send_message delivers to the legacy ::actor::receive_message name;
105
+ // this shim routes it into the core receive handler (→ on_message_received hook).
106
+ trn receive_message args: any { return a2a_messaging::handle_receive_message args. }
107
+ trn readonly list_incoming_messages _ { return ($inbox -> inbox). }
108
+
109
+ // Version probe: the compiled-in core version, observable at runtime.
110
+ trn readonly get_version _ { return ($core -> (version::get_core_version NIL)). }
111
+
112
+ // Migration: compose YOUR app state around the core's portable export.
113
+ trn readonly export_state _
114
+ {
115
+ return ($core -> (a2a_messaging::export_core_state NIL), $app -> ($inbox -> inbox)).
116
+ }
117
+ trn import_state data: any
118
+ {
119
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
120
+ a2a_messaging::import_core_state (data $core).
121
+ inbox -> ((data $app $inbox) safe (msg_t[])).
122
+ return transaction::success [ _return_data ($imported -> TRUE), _save_state NIL ].
123
+ }
124
+ }
125
+ ```
126
+
127
+ Syntax gotcha: typed-list casts need parentheses — `safe (msg_t[])`, not
128
+ `safe msg_t[]`.
129
+
130
+ 2. Recompile (remove the previous unit first; the output name is a content hash, so a
131
+ changed source produces a *second* `.muflo` otherwise):
132
+
133
+ ```sh
134
+ cd mufl_code
135
+ rm -f *.muflo
136
+ MUFL_STDLIB_PATH="$ADAPT_TOOLKIT/mufl_stdlib" \
137
+ "$ADAPT_TOOLKIT/build.linux.release/mufl-compile" \
138
+ -mp "$ADAPT_TOOLKIT/meta" -mp "$ADAPT_TOOLKIT/transactions" my_agent.mu
139
+ ```
140
+
141
+ **Verify:**
142
+
143
+ ```sh
144
+ ls *.muflo
145
+ ```
146
+
147
+ Success markers:
148
+
149
+ - Compiler ends with `SAVED TO FILE: <…muflo>`.
150
+ - `ls *.muflo` shows exactly one unit (a new hash — the old one is gone).
151
+
152
+ For what the hooks feed into — deferred sends, degraded contacts, restore — see
153
+ [Messaging](../how-it-works/messaging.md). Next:
154
+ [04 · Connect & message](./04-connect-and-message.md).
@@ -0,0 +1,156 @@
1
+ # 04 · Connect & message
2
+
3
+ Time to run the packet. The `@adapt-toolkit` Node SDK boots compiled units, a local
4
+ dev broker relays between them, and a small driver script walks the protocol:
5
+ `generate_invite` on one packet, `add_contact` on the other, then `send_message` both
6
+ ways — asserting **receiver-side** state (the peer's inbox and contact book), not just
7
+ that a send returned. This is the same loopback pattern the core's own
8
+ [`tests/test.mjs`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/test.mjs)
9
+ uses. For what the three legs of the invite redeem do on the wire, see
10
+ [Invites & contacts](../how-it-works/invites-and-contacts.md).
11
+
12
+ **Prereqs:**
13
+
14
+ - [03 · Wire the host](./03-wire-the-host.md) completed (wired `.muflo` in `mufl_code/`).
15
+ - `$OURS_SDK_NODE_MODULES` and `$DEV_BROKER` exported (see [Start here](./index.md)).
16
+ - Node 18+.
17
+
18
+ **Steps:**
19
+
20
+ 1. Make the SDK resolvable from `mufl_code/` (both the driver and the broker launcher
21
+ need a `node_modules` that contains `@adapt-toolkit`):
22
+
23
+ ```sh
24
+ cd mufl_code
25
+ ln -sfn "$OURS_SDK_NODE_MODULES" node_modules
26
+ ```
27
+
28
+ 2. Create `mufl_code/drive.mjs`:
29
+
30
+ ```js
31
+ #!/usr/bin/env node
32
+ // Loopback driver: two packets of YOUR app on one local broker.
33
+ // invite -> contact -> message, asserted RECEIVER-side.
34
+ import { resolve } from 'node:path';
35
+ import * as fs from 'node:fs';
36
+ import { adapt_wrapper } from '@adapt-toolkit/sdk/executables';
37
+ import { PacketWrapperConfigurator } from '@adapt-toolkit/sdk/wrappers';
38
+ import { object_to_adapt_value } from '@adapt-toolkit/sdk/wrapper';
39
+
40
+ const BROKER_URL = process.env.BROKER_URL || 'ws://127.0.0.1:9799';
41
+ const UNIT_DIR = resolve('.');
42
+ const unitHash = fs.readdirSync(UNIT_DIR).find((f) => f.endsWith('.muflo')).slice(0, -'.muflo'.length);
43
+ const UNIT = new Uint8Array(fs.readFileSync(resolve(UNIT_DIR, `${unitHash}.muflo`)));
44
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
45
+ let failures = 0;
46
+ const ok = (c, m) => { console.log(`${c ? 'ok' : 'FAIL'} - ${m}`); if (!c) failures++; };
47
+
48
+ let wrapper;
49
+ function mk(name) { return { name, pw: null, cid: '', pending: [] }; }
50
+ function wire(id) {
51
+ id.pw.on_return_data = (d) => {
52
+ const kind = d.Reduce('kind').Visualize();
53
+ if (kind !== 'data') return; // ignore save_state / notify_agent here
54
+ const p = id.pending.shift(); if (!p) return;
55
+ clearTimeout(p.timer); p.resolve(d.Reduce('payload'));
56
+ };
57
+ id.pw.on_transaction_failure = (msg) => {
58
+ const p = id.pending.shift();
59
+ if (p) { clearTimeout(p.timer); p.reject(new Error(msg)); }
60
+ };
61
+ }
62
+ function mutate(id, name, targ) {
63
+ return new Promise((res, rej) => {
64
+ const timer = setTimeout(() => rej(new Error(`${id.name}.${name} timed out`)), 20000);
65
+ id.pending.push({ resolve: res, reject: rej, timer });
66
+ id.pw.add_client_message(object_to_adapt_value({ name, targ }));
67
+ });
68
+ }
69
+ const ro = (id, name) => id.pw.packet.ExecuteTransaction(object_to_adapt_value({ name, targ: undefined }));
70
+ const binv = (id, buf) => id.pw.packet.NewBinaryFromBuffer(Buffer.from(buf));
71
+ async function mkPacket(id, seed) {
72
+ const cfg = new PacketWrapperConfigurator();
73
+ cfg.process_arguments(['--unit_hash', unitHash, '--seed_phrase', seed, '--unit_dir_path', UNIT_DIR]);
74
+ await new Promise((res, rej) => {
75
+ const t = setTimeout(() => rej(new Error(`${id.name} create timeout`)), 30000);
76
+ wrapper.packet_manager.create_packet(cfg, (pw) => {
77
+ clearTimeout(t); id.pw = pw; id.cid = pw.packet.GetContainerID().Visualize(); wire(id); res();
78
+ }, UNIT);
79
+ });
80
+ }
81
+
82
+ async function main() {
83
+ wrapper = await adapt_wrapper.start(['--broker_address', BROKER_URL, '--test_mode',
84
+ '--logger_config', '--level', 'WARNING', '--stdout', 'stderr', '--logger_config_end']);
85
+ wrapper.start();
86
+ await sleep(1500);
87
+
88
+ const A = mk('A'); const B = mk('B');
89
+ await mkPacket(A, 'my-app-A-01'); await mkPacket(B, 'my-app-B-02');
90
+ await sleep(1200);
91
+ await mutate(A, '::a2a_messaging::set_my_name', { name: 'Alice' });
92
+ await mutate(B, '::a2a_messaging::set_my_name', { name: 'Bob' });
93
+
94
+ // compiled-in core version is observable at runtime
95
+ console.log('core version:', ro(A, '::actor::get_version').Reduce('core').Visualize());
96
+
97
+ // 1. A mints an invite
98
+ const m = await mutate(A, '::a2a_messaging::generate_invite', { name: 'Bob' });
99
+ const invite = Buffer.from(m.Reduce('invite').GetBinary());
100
+ ok(invite.length > 0, 'generate_invite returned an invite blob');
101
+
102
+ // 2. B redeems it
103
+ await mutate(B, '::a2a_messaging::add_contact', { invite: binv(B, invite), name: 'Alice' });
104
+ await sleep(5000);
105
+
106
+ // 3. RECEIVER-side: both contact books list the other
107
+ const lcA = ro(A, '::a2a_messaging::list_contacts').Visualize();
108
+ const lcB = ro(B, '::a2a_messaging::list_contacts').Visualize();
109
+ ok(new RegExp(B.cid).test(lcA), 'A list_contacts includes B cid');
110
+ ok(new RegExp(A.cid).test(lcB), 'B list_contacts includes A cid');
111
+
112
+ // 4. message round-trips over the encrypted channel, both directions
113
+ await mutate(A, '::a2a_messaging::send_message', { contact: B.cid, text: 'hello from Alice' });
114
+ await sleep(2500);
115
+ ok(/hello from Alice/.test(ro(B, '::actor::list_incoming_messages').Visualize()),
116
+ 'B received A message (receiver-side inbox)');
117
+ await mutate(B, '::a2a_messaging::send_message', { contact: A.cid, text: 'hello from Bob' });
118
+ await sleep(2500);
119
+ ok(/hello from Bob/.test(ro(A, '::actor::list_incoming_messages').Visualize()),
120
+ 'A received B message (receiver-side inbox)');
121
+
122
+ console.log(failures === 0 ? 'ROUND-TRIP OK' : `${failures} FAILURE(S)`);
123
+ await sleep(500);
124
+ process.exit(failures === 0 ? 0 : 1);
125
+ }
126
+ main().catch((e) => { console.error('DRIVER ERR:', e.stack ?? e.message); process.exit(1); });
127
+ ```
128
+
129
+ Reading the driver: `mutate` submits a state-changing transaction through the
130
+ wrapper's client-message path and resolves on the `$kind -> $data` action your hooks
131
+ returned; `ro` executes a read-only transaction synchronously. The SDK's leak-tracker
132
+ prints `###` lines at exit — expected noise.
133
+
134
+ 3. Start the dev broker, run the driver, stop the broker:
135
+
136
+ ```sh
137
+ node "$DEV_BROKER" --host 127.0.0.1 --port 9799 --test_mode > broker.log 2>&1 &
138
+ sleep 3
139
+ BROKER_URL=ws://127.0.0.1:9799 node drive.mjs 2>/dev/null | grep -vE '^###'
140
+ echo "EXIT=${PIPESTATUS[0]}"
141
+ kill %1
142
+ ```
143
+
144
+ **Verify:** the driver prints a `core version:` line (the `MAJ`/`MIN`/`PATCH` of the
145
+ core revision you vendored — see [Versioning](../how-it-works/versioning.md)) and five
146
+ `ok -` assertion lines, ending with the success markers:
147
+
148
+ ```
149
+ ok - A received B message (receiver-side inbox)
150
+ ROUND-TRIP OK
151
+ EXIT=0
152
+ ```
153
+
154
+ Any `FAIL -` line or a non-zero `EXIT` means the round-trip did not complete.
155
+
156
+ Next: [05 · Test your app](./05-test-your-app.md).
@@ -0,0 +1,75 @@
1
+ # 05 · Test your app
2
+
3
+ Two layers of testing keep a consumer honest: a **loopback suite for your own app**,
4
+ and the **core's own suite** run against the exact core revision you vendored.
5
+
6
+ ## The loopback pattern for your app
7
+
8
+ Grow `drive.mjs` from page 04 into a real suite the way the core's tests do
9
+ (see [`tests/README.md`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/README.md)):
10
+
11
+ - **A derived test actor.** Don't test against your production actor alone — derive a
12
+ test build that adds `qa_*` probe transactions: read-only state counters
13
+ (`qa_state`-style counts of contacts, peer ADs, pending invites), exporters
14
+ (`qa_export_ad`), and adversarial injectors that bare-send crafted wire payloads. The
15
+ core's [`tests/test_actor.mu`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/tests/test_actor.mu)
16
+ shows the full probe vocabulary.
17
+ - **Assert receiver-side.** A send that "succeeded" proves little; assert the *peer's*
18
+ state changed (its inbox, its contact book) and that rejected inputs changed
19
+ *nothing* — the core suite's negative scenarios (replayed invites, tampered boxes,
20
+ foreign address documents) all assert state-unchanged on the target.
21
+ - **One broker, many packets.** Spin all packets on one `dev-broker.mjs` in
22
+ `--test_mode`; inbound aborts surface via `on_transaction_failure`, so collect them
23
+ per-packet and grep them in assertions.
24
+
25
+ ## Run the core's own suite (sanity check)
26
+
27
+ The vendored submodule ships its suite: 10 scenarios, 36+ assertions over invite
28
+ redeem, tamper rejection, export secrecy, and migration
29
+ (see [Invites & contacts](../how-it-works/invites-and-contacts.md)). Running it against
30
+ your checkout proves your toolkit + SDK + broker environment is sound and the core
31
+ revision you pinned behaves as released.
32
+
33
+ **Prereqs:**
34
+
35
+ - Pages [01](./01-vendor-the-core.md)–[04](./04-connect-and-message.md) completed.
36
+ - `$ADAPT_TOOLKIT`, `$OURS_SDK_NODE_MODULES`, `$DEV_BROKER` exported.
37
+ - No leftover dev broker on the chosen port (the suite boots its own; `PORT=9791`
38
+ below avoids clashing with page 04's broker if you left it running).
39
+
40
+ **Steps:**
41
+
42
+ 1. From your app repo **root** (`my-app`, not `mufl_code/`), run the suite inside the
43
+ submodule, pointing it at your
44
+ environment (`tests/run.sh` honors these as env overrides; it bundles the
45
+ `protocol_container.mm` stub itself, so you no longer pass one in):
46
+
47
+ ```sh
48
+ cd mufl_code/core
49
+ ADAPT_TOOLKIT="$ADAPT_TOOLKIT" \
50
+ OURS_SDK_NODE_MODULES="$OURS_SDK_NODE_MODULES" \
51
+ DEV_BROKER="$DEV_BROKER" \
52
+ PORT=9791 ./tests/run.sh
53
+ ```
54
+
55
+ The run takes a few minutes: it copies the core into a throwaway harness, compiles
56
+ the test actor, boots a broker on `PORT`, and drives all scenarios. `###` /
57
+ `Leak for AdaptValue` lines and `EVAL_ERROR` stderr noise are expected — the
58
+ adversarial scenarios deliberately trigger aborts.
59
+
60
+ **Verify:**
61
+
62
+ ```sh
63
+ echo "EXIT=$?"
64
+ ```
65
+
66
+ Success markers (the verdict is the final scorecard plus the exit code):
67
+
68
+ ```
69
+ ================ SCORECARD ================
70
+ ALL TESTS PASSED
71
+ EXIT=0
72
+ ```
73
+
74
+ That closes the loop: an empty directory, a vendored core, a compiled packet, a live
75
+ encrypted round-trip, and the protocol's own suite green against your environment.
@@ -0,0 +1,63 @@
1
+ # Start here
2
+
3
+ > **These docs are written for agents.** ours.network expects most applications to be
4
+ > built by coding agents working on behalf of a human. Pages are structured as executable
5
+ > runbooks — exact paths, copy-paste commands, verification steps — rather than narrative
6
+ > tutorials. Humans are welcome; give your agent the URL of
7
+ > [`llms-full.txt`](https://adapt-toolkit.github.io/ours-mufl-core/llms-full.txt) and it can ingest the entire
8
+ > documentation in one fetch.
9
+
10
+ This guide takes you from an empty directory to a working ours.network application:
11
+ a MUFL packet that **consumes** the shared protocol core, plus the host-side driver
12
+ that boots it, connects it to a peer, and exchanges encrypted messages. Every
13
+ ours.network client is built this way — it vendors this repo as a git submodule and
14
+ compiles it into its own packet (see [Overview](../how-it-works/overview.md)).
15
+
16
+ **Scope note — integration only.** This guide never touches protocol code. You will not
17
+ edit any `.mm` file of the core; a change there is a protocol revision for the whole
18
+ network. Everything you write here — your `config.mufl`, your actor (`.mu` file), your
19
+ Node driver — is *your application*, layered on top of an unmodified core. If you think
20
+ the protocol itself needs a change, read
21
+ [Contributing](../reference/contributing.md) instead.
22
+
23
+ ## What you will build
24
+
25
+ | Page | Result |
26
+ |------|--------|
27
+ | [01 · Vendor the core](./01-vendor-the-core.md) | The core checked out as a git submodule under `mufl_code/core` |
28
+ | [02 · Configure & compile](./02-configure-and-compile.md) | A `config.mufl` that merges the core with the stdlib, and a first compiled packet |
29
+ | [03 · Wire the host](./03-wire-the-host.md) | An actor with the storage hooks, init wiring, and export/import composition the core expects |
30
+ | [04 · Connect & message](./04-connect-and-message.md) | Two packets on a local broker: invite → contact → encrypted message round-trip |
31
+ | [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 |
32
+
33
+ **Prereqs:**
34
+
35
+ - The **ADAPT toolkit** — the `mufl-compile` binary plus the `mufl_stdlib` / `meta` /
36
+ `transactions` module trees. The pages refer to its root as `$ADAPT_TOOLKIT`
37
+ (`$ADAPT_TOOLKIT/build.linux.release/mufl-compile` must exist).
38
+ - The **`@adapt-toolkit` Node SDK** — a `node_modules` directory containing
39
+ `@adapt-toolkit` (any ours.network consumer checkout has one). The pages refer to it
40
+ as `$OURS_SDK_NODE_MODULES`.
41
+ - A **local dev broker launcher** — `dev-broker.mjs`, a thin launcher over the SDK's
42
+ broker exports (ships with the consumer repos). The pages refer to it as `$DEV_BROKER`.
43
+ It must run from a directory whose `node_modules` resolves `@adapt-toolkit`; the
44
+ layout built in this guide takes care of that.
45
+ - **git** and **Node 18+**.
46
+
47
+ These are the same three knobs the core's own test suite takes
48
+ (`ADAPT_TOOLKIT` / `OURS_SDK_NODE_MODULES` / `DEV_BROKER` — see
49
+ [05 · Test your app](./05-test-your-app.md)). Export them once per shell:
50
+
51
+ ```sh
52
+ export ADAPT_TOOLKIT=/path/to/adapt-toolkit
53
+ export OURS_SDK_NODE_MODULES=/path/to/consumer/node_modules
54
+ export DEV_BROKER=/path/to/dev-broker.mjs
55
+ ```
56
+
57
+ **Steps:** start at [01 · Vendor the core](./01-vendor-the-core.md); each page builds
58
+ on the previous one and ends with a Verify block whose success markers come from a
59
+ live run of exactly the commands shown.
60
+
61
+ **Verify:** after page 04 you have a bidirectional encrypted round-trip
62
+ (`ROUND-TRIP OK`, exit 0); after page 05 the core's own suite reports
63
+ `SCORECARD` / `ALL TESTS PASSED` with exit 0.
@@ -0,0 +1,77 @@
1
+ # Capabilities & control
2
+
3
+ Every ours node advertises a typed app manifest — a set of named capabilities,
4
+ each with its own schema and version — and receives structured requests through a
5
+ shared control envelope. `a2a_capabilities.mm` owns the envelope definition and
6
+ dispatch; `a2a_control.mm` owns the encrypted transport that carries envelopes
7
+ between nodes.
8
+
9
+ Sources:
10
+ [`a2a_capabilities.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_capabilities.mm),
11
+ [`a2a_control.mm`](https://github.com/adapt-toolkit/ours-mufl-core/blob/main/a2a_control.mm).
12
+
13
+ ## The control envelope
14
+
15
+ All inbound capability requests arrive as a `control_envelope_t` record:
16
+
17
+ ```mufl
18
+ metadef control_envelope_t: ($cap -> str, $verb -> str, $args -> any, $req_id -> str).
19
+ ```
20
+
21
+ - `$cap` — the stable capability id (e.g. `"core.configuration"`). Dispatch keys
22
+ only on this field; adding a capability never changes the wire shape.
23
+ - `$verb` — the operation within that capability.
24
+ - `$args` — native-typed arguments the handler interprets per verb.
25
+ - `$req_id` — correlation token. The sender receives a `response_envelope_t`
26
+ keyed on `(sender_id, $req_id)`. An empty `$req_id` signals fire-and-forget:
27
+ the envelope is processed but no response is expected.
28
+
29
+ ## Transport
30
+
31
+ `send_control` in `a2a_control.mm` delivers an opaque payload to a named contact
32
+ over the `encrypted_channel`. The inbound side is the `control_message`
33
+ transaction, which validates origin and sender before invoking the
34
+ `$on_control_received` hook the app wires at startup.
35
+
36
+ The `a2a_capabilities` dispatch layer sits one step above this transport: the
37
+ app adapts the opaque `control_message` payload into a `control_envelope_t`
38
+ record and calls `dispatch`.
39
+
40
+ ## Dispatch and the authz chokepoint
41
+
42
+ `dispatch` enforces a single, non-bypassable pre-route authorization check
43
+ before any handler runs:
44
+
45
+ 1. `control_auth_class($cap, $verb)` classifies the requested operation as
46
+ `"bootstrap"`, `"controller"`, or `"deny"`. (`get_manifest` is a standalone
47
+ `trn readonly` that never routes through dispatch and is not classified here.)
48
+ 2. `"controller"` verbs require the stateful `authorizer` gate (wired at init via
49
+ `a2a_messaging::authorize_control`) to confirm the sender is the bound control
50
+ plane. The gate is fail-closed: an unset authorizer aborts rather than
51
+ permitting the verb.
52
+ 3. Unknown or unlisted cap/verb combinations always classify as `"deny"` — a new
53
+ verb must be consciously listed in `control_auth_class` to become reachable.
54
+
55
+ Handlers run only after this chokepoint clears. They return
56
+ `transaction::action::type[]` arrays; the daemon marshals responses to JSON and
57
+ ships them — no in-MUFL JSON encoding, no ad-hoc send inside a handler.
58
+
59
+ ## Well-known capability ids
60
+
61
+ Four ids are reserved in `a2a_capabilities.mm`:
62
+
63
+ | Constant | Value | Purpose |
64
+ |---|---|---|
65
+ | `cap_configuration` | `"core.configuration"` | Opaque app config, read/written only by the bound control plane. |
66
+ | `cap_monitoring` | `"core.monitoring"` | Monitoring bind ceremony and disable. |
67
+ | `cap_connect` | `"core.connect"` | Peer introduction via the control plane. |
68
+ | `cap_cluster` | `"core.cluster"` | Child/subagent lifecycle and contact management. |
69
+
70
+ The `"core.*"` namespace is reserved; application capabilities use `"app.*"`.
71
+ Every node advertises `core.monitoring` — it is governance-required and
72
+ auto-present. Adding a new capability means registering a new id string and
73
+ wiring its handler; the wire shape is unchanged.
74
+
75
+ See [Cluster](./cluster.md) for the `core.cluster` verb surface, and
76
+ [Monitoring & config](./monitoring-and-config.md) for how `core.monitoring` and
77
+ `core.configuration` interact with the hidden gate state in `a2a_messaging`.