@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,290 @@
1
+ # NOTIFICATIONS_CONTRACT — the `a2a_notifications` surface (core 0.4)
2
+
3
+ The frozen protocol contract for Notifications-as-a-Service: a
4
+ protocol-defined **notification-service node** (an always-online packet any
5
+ operator can run) plus a `send_notification` transaction parallel to
6
+ `send_message`, so any ours identity can register for out-of-band wake-ups
7
+ (WebPush), hand out **per-contact tokens**, and each token-holder can trigger
8
+ a push to the recipient's browser(s) — even while the recipient's own packet
9
+ is offline.
10
+
11
+ Tokens are **per-contact only**: every token is scoped to exactly one
12
+ (recipient, sender) pair via `$scope == _str(sender_cid)`. There is no shared
13
+ token, no unscoped fallback, and registration itself mints nothing — tokens
14
+ come exclusively from `issue_tokens`.
15
+
16
+ Library: [`a2a_notifications.mm`](./a2a_notifications.mm). Capability id:
17
+ `core.notifications` (`a2a_capabilities.mm` — reserved constant only; **no
18
+ control verbs**, so `control_auth_class`/`dispatch` are untouched).
19
+
20
+ ---
21
+
22
+ ## ⚠️ Trust model (READ THIS FIRST)
23
+
24
+ - **The service reads every notification payload. There is NO end-to-end
25
+ privacy from the service.** The payload travels encrypted on each transport
26
+ leg (the wire's signed/encrypted envelopes; WebPush's own `aes128gcm`
27
+ payload encryption), but the service node decrypts and sees the full
28
+ plaintext, and stores it in its app-side log. Do not put secrets in
29
+ notification payloads you would not show the service operator.
30
+ - **There is NO rate limiting / anti-spam.** A contact holding their handout
31
+ can notify the recipient without limit. The recovery paths are per-contact:
32
+ `rotate_token ($sender)` for a known abuser (their old handout dies
33
+ instantly), `revoke_sender_tokens` for contact removal (no re-mint), and
34
+ rotate-all as the panic button for a leak of unknown origin. A leak only
35
+ ever exposes ONE relationship's channel.
36
+ - **Sender authorization is scoped-token possession PLUS sender binding.**
37
+ `post_notification` is accepted as a **bare signed send** from a packet the
38
+ service has never met (no contact, no encrypted channel — the invite-leg
39
+ precedent). The envelope `$from` is wrapper-verified by the send primitive
40
+ and MUST equal the token's `$scope` — a stolen handout is useless without
41
+ the named sender's signing key. `$from` is also recorded on the notification
42
+ (`$sender_cid`), giving per-relationship attribution.
43
+ - **The service learns per-contact mute flags** (which of a recipient's
44
+ contacts are muted). The service already sees `$sender_cid` on every post,
45
+ so the mute flags are the marginal disclosure.
46
+ - The recipient↔service surface (register / update_bindings / issue_tokens /
47
+ rotate / set_sender_muted / revoke / unregister / mark_read / confirm) rides
48
+ the **encrypted channel** and requires the peers to be contacts —
49
+ established by the ordinary invite/introduction machinery; no new
50
+ connection flow.
51
+ - The **VAPID private key never enters packet state** (and therefore never
52
+ rides `export_notify_state`). It lives only in the host daemon's env/file.
53
+ The packet holds the *public* key (`set_vapid_public_key`) to echo to
54
+ clients.
55
+
56
+ ## Wire shapes (frozen bytes — never rename/retype a `$field`)
57
+
58
+ ```mufl
59
+ // The per-contact sender token, minted and SIGNED by the service.
60
+ metadef notify_token_core_t: (
61
+ $version -> int, // 1
62
+ $service_cid -> global_id, // the issuing service (binds token to one N)
63
+ $recipient_cid -> global_id, // whom it notifies
64
+ $token_id -> global_id, // rotation/revocation handle
65
+ $scope -> str, // _str of the SENDER cid this token is issued to
66
+ $iat -> time
67
+ ).
68
+ metadef notify_token_t: ($c -> notify_token_core_t, $s -> crypto_signature).
69
+
70
+ // One WebPush subscription (browser PushSubscription fields).
71
+ metadef webpush_binding_t: (
72
+ $version -> int, // 1
73
+ $binding_id -> str, // app-chosen stable id (e.g. endpoint hash)
74
+ $endpoint -> str, // push service URL
75
+ $p256dh -> str, // client public key, base64url
76
+ $auth -> str // auth secret, base64url
77
+ ).
78
+
79
+ // The handout blob R gives to ONE sender ("to notify me: this service, YOUR token").
80
+ metadef notify_address_t: (
81
+ $version -> int, // 1
82
+ $service_cid -> global_id, // bare-send routing target
83
+ $service_name -> str, // display only
84
+ $token -> notify_token_t
85
+ ).
86
+
87
+ // SERVICE-side registration record (core state on N). Carries NO token —
88
+ // per-contact tokens live in notify_sender_tokens, minted only by issue_tokens.
89
+ metadef registration_t: (
90
+ $version -> int, // 1
91
+ $recipient_cid -> global_id,
92
+ $bindings -> webpush_binding_t[],
93
+ $created_at -> time
94
+ ).
95
+
96
+ // CLIENT-side view of my registration (core state on R).
97
+ metadef my_registration_t: (
98
+ $service_cid -> global_id,
99
+ $service_name -> str,
100
+ $vapid_pub -> str, // service's VAPID public key (browser subscribe param)
101
+ $bindings -> webpush_binding_t[],
102
+ $created_at -> time
103
+ ).
104
+
105
+ // One posted notification as handed to the service app hook (LOG is app-side).
106
+ metadef notification_t: (
107
+ $version -> int, // 1
108
+ $notif_id -> str, // service-stamped — the mark_read handle
109
+ $wire_id -> str, // sender-stamped, "" tolerated
110
+ $recipient_cid -> global_id,
111
+ $sender_cid -> global_id, // envelope $from (authenticated; == token $scope by validation)
112
+ $payload -> str, // OPAQUE to the protocol; the service reads it
113
+ $date -> time
114
+ ).
115
+ ```
116
+
117
+ Payload limit: `payload_max_bytes = 4000` (WebPush ceiling is ~4KB). Oversize
118
+ aborts on the **sender** side (`send_notification`) AND the **service** side
119
+ (`post_notification`) — defense in depth (E6).
120
+
121
+ Issuance batch limit: `issue_max_senders = 256` per `issue_tokens` call;
122
+ oversized batches abort (V12). The client wrapper pages automatically.
123
+
124
+ Inbound routing: every inbound name is **library-routed**
125
+ `::a2a_notifications::<name>` (no `::actor::` shims).
126
+
127
+ ## Transaction surface
128
+
129
+ ### Client half (compiled into every consumer)
130
+
131
+ | trn | args | behavior |
132
+ |---|---|---|
133
+ | `notify_register` | `$service -> str`, `$bindings -> webpush_binding_t[]+` | resolve the service via `a2a_messaging::resolve_contact`; encrypted send `register`; mark pending. `$bindings` NIL is valid (E11 — register first, bind after the browser subscribes). Registration mints NO token. |
134
+ | `notify_update_bindings` | `$service -> str`, `$bindings -> webpush_binding_t[]` | must hold a registration; encrypted send `update_bindings` (**replace-all** semantics). |
135
+ | `notify_issue_tokens` | `$service -> str`, `$contacts -> global_id[]` | must hold a registration; pages `$contacts` into batches of `issue_max_senders`; one encrypted `issue_tokens` send per batch. |
136
+ | `notify_rotate_token` | `$service -> str`, `$contact -> str+` | must hold a registration. `$contact` present → resolve via `resolve_contact`, encrypted send `rotate_token ($sender)` (rotate ONE contact's token). `$contact` NIL → `rotate_token` with empty targ (rotate-ALL panic button). Marks pending. |
137
+ | `notify_set_sender_muted` | `$service -> str`, `$contact -> global_id`, `$muted -> bool` | must hold a registration; encrypted send `set_sender_muted ($sender, $muted)`. Runtime-only toggle — no token change, no redistribution. |
138
+ | `notify_revoke_contact_tokens` | `$service -> str`, `$contacts -> global_id[]` | must hold a registration; encrypted send `revoke_sender_tokens ($senders)` (delete-without-remint — the contact-removal path). |
139
+ | `notify_unregister` | `$service -> str` | encrypted send `unregister`; clears the local registration + pending. |
140
+ | `notify_mark_read` | `$service -> str`, `$notif_ids -> str[]+` | must hold a registration; encrypted send `mark_read`. ids `NIL` ⇒ **mark ALL read** (the on-open default). "Dismiss" == mark_read. |
141
+ | `send_notification` | `$address -> bin`, `$payload -> str` | parse the handout (version == 1); abort oversize; stamp a `wire_id` (shared `_new_id` namespace); **ONE bare signed send** of `post_notification` to the blob's `$service_cid`; `_return_data ($sent_to, $wire_id)`. **No monitoring copy** (D-4) — the forced-monitoring contract covers *messages*. Fire-and-forget: no response leg. |
142
+ | `export_notify_address` (readonly) | `$service -> str`, `$contact -> str` | the per-contact handout blob (`_write` of `notify_address_t` wrapping `my_notify_contact_tokens[service][contact]`). Aborts if no scoped token exists yet (`issue_tokens` first). Distribution rides ordinary messaging or the messenger's distribution engine. |
143
+ | `confirm_registration` (INBOUND, external + encrypted) | vapid_pub, bindings, sender_tokens, sender_muted | `$from` must be a **pending or already-registered** service (E9 — an unsolicited confirm cannot plant a registration); stores `my_registration_t`; replaces `my_notify_contact_tokens[service]` **wholesale** from `$sender_tokens`; clears pending; fires `on_notify_registration` with the token AND mute maps. |
144
+
145
+ ### Service half (a notifier packet)
146
+
147
+ All recipient-surface inbounds require an **encrypted channel**
148
+ (`check_encrypted_or_abort`) and — except `register` — a **live registration**
149
+ for the sender (abort `"No notification registration for this sender."`, the
150
+ shared gate pattern).
151
+
152
+ | trn | channel | behavior |
153
+ |---|---|---|
154
+ | `set_vapid_public_key` | user-origin (host boot) | store the PUBLIC key echoed in every confirm. |
155
+ | `register` | encrypted required | recipient = envelope `$from`; store a registration record (**mints NOTHING** — tokens come only from `issue_tokens`); reply `confirm_registration`. **Re-register is idempotent** (E8): existing scoped tokens untouched, bindings replaced only when provided, re-confirm either way. |
156
+ | `update_bindings` | encrypted required | must be registered; replace bindings wholesale; re-confirm (idempotent). |
157
+ | `issue_tokens` | encrypted required | `$senders -> global_id[]+` (non-empty, ≤ 256 — V11/V12). For each sender: existing scoped token → **kept** (idempotent, E8); else `mint_notify_token_scoped recipient (_str sender)`, store in `notify_sender_tokens[recipient][sender]`, index `token_id → recipient`. Re-confirms with the full maps. |
158
+ | `rotate_token` | encrypted required | `$sender` present → delete ONLY that sender's old `token_id` index entry, mint/store/index a fresh scoped token (abort `"No scoped token for this sender."` when the slot is absent). `$sender` absent → **rotate-all**: every scoped slot, two-pass (collect keys, then rotate). Old handouts die instantly (E4). Re-confirms. |
159
+ | `set_sender_muted` | encrypted required | `$muted TRUE` → write a flag entry into `notify_sender_muted[recipient][sender]` (**present = muted**); `$muted FALSE` → delete the entry (**absent = enabled**, minimal state). No token change. Re-confirms. |
160
+ | `revoke_sender_tokens` | encrypted required | for each sender: delete its scoped `token_id` from the index (posts abort at step 3 — E4) AND its `notify_sender_tokens` slot. **No re-mint.** Idempotent: unknown senders tolerated. Re-confirms. |
161
+ | `unregister` | encrypted required | full teardown: registration, EVERY scoped token (slot + index entry) and the mute map removed; fire `on_unregistered`. Re-register + issue_tokens starts from a clean slate. |
162
+ | `post_notification` | **bare signed accepted** (no `check_encrypted_or_abort`) | validate the token (order below); build `notification_t`; fire `on_notification_posted` with the recipient's current bindings; **NO reply**. |
163
+ | `mark_read` | encrypted required | `$from` must hold a registration; fire `on_notifications_marked_read` with `($recipient_cid, $notif_ids /* NIL = ALL */)` — only ever the caller's own cid, so one recipient cannot mark another's log (E7). |
164
+
165
+ ### Token validation in `post_notification` (order matters; abort on first failure, mutate nothing)
166
+
167
+ 1. Parse: `$token` `safe notify_token_t`; `$c $version == 1`.
168
+ 2. `$c $service_cid == _get_container_id()` — minted by THIS service.
169
+ 3. Index lookup: `notify_token_index ($c $token_id)` must exist and equal
170
+ `$c $recipient_cid` — rotation/revocation/unregister removed entries, so
171
+ **revocation is a state lookup**, not a signature question. Additionally
172
+ `notify_registrations ($c $recipient_cid)` must exist.
173
+ 4. **Sender binding:** envelope `$from` must satisfy
174
+ `$scope == _str($from)` — abort **`"Token is not bound to this sender."`**
175
+ A token with any other scope (including the empty string) dies here.
176
+ (Binding is evaluated before byte-equality: mufl cannot reverse
177
+ `_str(sender_id)` back to a `global_id`, so the bound identity comes from
178
+ the envelope; observably equivalent to checking equality first.)
179
+ 5. **Byte-equality** against the stored scoped token:
180
+ `_value_id (presented) == _value_id (notify_sender_tokens[recipient][sender])`
181
+ — absent outer map / absent sender slot abort
182
+ `"Token is not bound to this sender."` (never-issued or revoked); a
183
+ mismatch aborts `"Presented token does not match the stored registration."`
184
+ Forging any field (`$scope` included) requires possessing the exact minted
185
+ artifact. No signature re-verification on the service: it compares against
186
+ what it itself stored. (The signature exists so *clients* can verify a
187
+ handout against the service's pinned keys.)
188
+ 6. **Mute check:** `notify_sender_muted[recipient][sender]` — if the entry is
189
+ **present** (present = muted, absent = enabled) → abort
190
+ **`"Notifications from this sender are disabled."`** Nothing stored,
191
+ nothing pushed. The abort is invisible to the sender:
192
+ `post_notification` has no reply leg, so mute state cannot be probed.
193
+ 7. Payload size ≤ `payload_max_bytes`.
194
+
195
+ ## Confirm shape (`confirm_registration` targ)
196
+
197
+ Every service-side mutator (register / update_bindings / issue_tokens /
198
+ rotate_token / set_sender_muted / revoke_sender_tokens) replies with the same
199
+ idempotent confirm:
200
+
201
+ ```
202
+ $vapid_pub -> str
203
+ $bindings -> webpush_binding_t[]
204
+ $sender_tokens -> (global_id ->> notify_token_t) // full current per-sender map
205
+ $sender_muted -> (global_id ->> bool) // full current mute map
206
+ ```
207
+
208
+ The client replaces `my_notify_contact_tokens[service]` **wholesale** from
209
+ `$sender_tokens` on each confirm — the confirm is the single source of truth
210
+ and re-confirms are idempotent replays (E8 recovery: a lost confirm is healed
211
+ by any later confirm).
212
+
213
+ ## State & hooks
214
+
215
+ Client state: `my_notify_registrations (global_id ->> my_registration_t)`,
216
+ `pending_notify_registers (global_id ->> bool)` (transient),
217
+ `my_notify_contact_tokens (global_id ->> (global_id ->> notify_token_t))`.
218
+ Service state: `notify_registrations (global_id ->> registration_t)`,
219
+ `notify_token_index (global_id ->> global_id)`,
220
+ `notify_sender_tokens (global_id ->> (global_id ->> notify_token_t))`,
221
+ `notify_sender_muted (global_id ->> (global_id ->> bool))`,
222
+ `vapid_public_key str`. Non-hidden (the contacts/peer_ads posture); only the
223
+ hooks are hidden.
224
+
225
+ `init` wires `$_read_or_abort` + four app hooks (unset hooks abort):
226
+ `on_notification_posted ($notification, $bindings)` ·
227
+ `on_notifications_marked_read ($recipient_cid, $notif_ids /*NIL=all*/)` ·
228
+ `on_unregistered ($recipient_cid)` · `on_notify_registration ($service_cid,
229
+ $registration, $sender_tokens, $sender_muted)` — the token and mute maps let
230
+ a consumer (e.g. the messenger's distribution engine) diff/mirror per-contact
231
+ state on every confirm. Consumers wire no-ops for the half they don't play.
232
+
233
+ Persistence: `export_notify_state` / `import_notify_state` — compose them into
234
+ the app's `export_state`/`import_state` beside `export_core_state`. Every
235
+ field is individually guarded on import (absent → default stays). The export
236
+ contains **no secret material by construction**.
237
+
238
+ ## Edge cases (the frozen abort/tolerate matrix)
239
+
240
+ | # | Case | Behavior |
241
+ |---|---|---|
242
+ | E1 | recipient-surface trn arriving unencrypted / from a non-contact | abort (`check_encrypted_or_abort`). |
243
+ | E2 | `post_notification` with unparseable / version≠1 token | abort, no state change. |
244
+ | E3 | token minted by another service | abort. |
245
+ | E4 | rotated/revoked token (`token_id` not indexed) | abort — this IS revocation. |
246
+ | E5 | token fields differ from the stored artifact (`_value_id` mismatch) | abort (forgery). |
247
+ | E6 | payload > 4000 | abort on sender AND service. |
248
+ | E7 | `mark_read` with foreign/nonexistent ids | hook only ever gets the caller's own cid; the daemon ignores non-matching ids (idempotent). |
249
+ | E8 | re-`register` while registered | keep scoped tokens, replace bindings when provided, re-confirm. Re-`issue_tokens` keeps existing tokens (byte-stable). |
250
+ | E9 | `confirm_registration` from a non-pending, non-registered sender | abort (cannot plant a registration). |
251
+ | E10 | duplicate `post_notification` (same `wire_id`) | delivered twice — **no dedup**, documented. |
252
+ | E11 | registration with zero bindings | valid — logged, nothing pushed, bindings added later. |
253
+ | E12 | WebPush endpoint 404/410/5xx | daemon logs and continues (binding auto-prune is a later enhancement). |
254
+ | E13 | daemon restart | packet state restores via the export/import helpers; the log is the daemon's own file; in-flight posts lost at most once (fire-and-forget contract). |
255
+ | E14 | `$from` spoofing concern | `$from` is wrapper-verified by the send primitive (not forge-able by payload manipulation) and is **authorization-grade**: it must equal the token's `$scope` (validation step 4). |
256
+ | E15 | consumers compiled without this library | unaffected — inbound names are library-routed and never sent to them. |
257
+ | V1 | `issue_tokens` for a cid that is not the recipient's contact | Token is minted (the service cannot and must not know the recipient's contact set; any cid R names is eligible). Harmless orphan; cleaned by rotate-all or `revoke_sender_tokens`. |
258
+ | V2 | Contact removed after their scoped token was issued | Host fires `notify_revoke_contact_tokens [cid]` (best-effort). If that call never lands (service unreachable), the orphan token remains valid only for posts from that specific sender cid (sender binding). Rotate-all clears stragglers. |
259
+ | V3 | `post_notification` from a muted sender | Aborts at step 6; nothing stored or pushed; invisible to the sender (no reply leg). |
260
+ | V4 | Token presented with `$from ≠ $scope` | Aborts at step 4 (`"Token is not bound to this sender."`); logged by the daemon via the existing inbound-rejected path. |
261
+ | V5 | Confirm lost after `issue_tokens` | Client re-calls `issue_tokens` — idempotent (existing tokens kept, E8). Re-confirm replays the full per-sender map. |
262
+ | V6 | Distribution while a contact is degraded (no peer address document) | The messenger engine skips that contact; its ledger entry stays dirty; retried when the address document returns. |
263
+ | V7 | A peer that ignores the handout's `$gen` field | Stores the blob (shape-identical), never acks; the engine's retry cap stops resends. Posting still works — the scoped token is valid for posts from its cid. |
264
+ | V9 | Two devices / stale blob overwrite (LWW hazard) | Ledger and token maps ride the same blob; LWW skew can resend handouts (idempotent) or re-issue (idempotent — E8). Converges. Pre-existing accepted hazard. |
265
+ | V10 | Service switch mid-flight while a rotation distribution is pending on the old service | `notify_mark_dirty` with the new `$service_cid` supersedes the in-flight generation. Per-contact monotonic generation numbers mean late acks from the old service cannot regress the new service's state (`max()` in the ack handler). |
266
+ | V11 | Recipient with zero contacts binds | Backfill (`issue_tokens` over contacts) is a no-op; no `issue_tokens` call is made. The first `contact_added` event triggers the normal per-contact issue + distribute path. |
267
+ | V12 | `issue_tokens` with more than 256 senders in one call | Service aborts with `"issue_tokens batch exceeds the 256-sender cap — V12."` (defense in depth). The client wrapper pages automatically; callers that bypass the wrapper must page themselves. |
268
+ | V13 | Duplicate `post_notification` (same `wire_id`) | == E10 — delivered twice, no dedup. |
269
+ | V14 | Recipient unbinds a newly-switched-to service B during the A→B overlap window | Registrations are independent; ledger entries pointing at B go stale. A later `notify_mark_dirty` toward A restores coherent state. The UI surfaces pending contacts. |
270
+
271
+ ## Non-goals (explicit)
272
+
273
+ - **Payload privacy from the service.** The service decrypts and reads every
274
+ notification payload. Do not put secrets in payloads you would not show the
275
+ service operator.
276
+ - **Rate limiting / anti-spam.** No rate limit is enforced on
277
+ `post_notification`. The recovery path for abuse is per-contact rotation or
278
+ revocation; rotate-all for a leak of unknown origin.
279
+ - Notification dedup (E10), WebPush binding auto-prune (E12), non-contact
280
+ ("guest") senders, multi-device real sync.
281
+
282
+ ## Versioning
283
+
284
+ The per-contact `a2a_notifications` surface ships in core **0.4.0**. Relative
285
+ to core 0.3 this is a **breaking rewrite of the notifications library**: the
286
+ shared (unscoped) token model was removed entirely — `registration_t` /
287
+ `my_registration_t` / the confirm no longer carry a `$token`, `register` mints
288
+ nothing, `export_notify_address` is per-contact, and `retire_shared` (which
289
+ never shipped) does not exist. There are no deployed v1-notifications clients;
290
+ consumers are (re)built against 0.4. See `release-notes/0.4.md`.
@@ -0,0 +1,91 @@
1
+ # ours-mufl-core — the shared agent-to-agent protocol core for ours.network
2
+
3
+ **[ours.network](https://ours.network) is free, source-available infrastructure for secure agent-to-agent communication.**
4
+
5
+ `ours-mufl-core` is the protocol at its heart: the shared agent-to-agent wire
6
+ format and verification logic, written as a set of MUFL (`.mm`) libraries. A
7
+ change here is a protocol revision for the whole network — because every client
8
+ links the same libraries, they all speak an identical wire format and
9
+ verification logic.
10
+
11
+ ## What's here / how it's used
12
+
13
+ The repo is a set of pure MUFL libraries with no standalone build. The modules:
14
+
15
+ | File | Purpose |
16
+ |------|---------|
17
+ | `a2a_capabilities.mm` | The app manifest, the capability/verb envelope and dispatch, and the well-known capabilities (`core.configuration`, `core.monitoring`, `core.connect`, `core.cluster`). |
18
+ | `a2a_protocol.mm` | Wire-facing shapes (invites, delegation certificates, root profiles, contact roots, introduction credentials) and the shared verification helpers. |
19
+ | `a2a_messaging.mm` | Contact and messaging transactions (generate invite, add/remove contact, send message, send file, inbound receive) and the introduction flow. |
20
+ | `a2a_cluster.mm` | The `core.cluster` capability: child/subagent lifecycle, per-child monitoring authorization, the host-local contact book, and introductions. |
21
+ | `a2a_monitoring.mm` | The receiver side of monitoring copies. |
22
+ | `a2a_control.mm` | Control-plane transport: an opaque payload sent to a contact over the encrypted channel. |
23
+ | `a2a_notifications.mm` | Notification service protocol: per-contact scoped tokens, WebPush bindings, issuance/rotation/revocation, receive-mute, and the bare signed-send notification ingest. |
24
+ | `version.mm` | The core version, exposed via `get_core_version`. |
25
+ | `config.mufl` | Exports the libraries above for `config_load #"core"`. |
26
+
27
+ It is consumed by the ours.network clients — the MCP agent server, the
28
+ human-facing web messenger, and the Telegram connector — each of which vendors
29
+ this repo as a **git submodule** and compiles it into its own packet:
30
+
31
+ ```sh
32
+ git submodule add git@github.com:adapt-toolkit/ours-mufl-core.git mufl_code/core
33
+ ```
34
+
35
+ The consumer's `config.mufl` merges the core's exports with the MUFL stdlib via
36
+ `config_load #"core"`, and its application loads the libraries by name:
37
+
38
+ ```mufl
39
+ application actor loads libraries ..., a2a_protocol, version uses transactions
40
+ ```
41
+
42
+ Each packet exposes the compiled-in version through its read-only `get_version`
43
+ transaction, so the deployed core version is observable at runtime.
44
+
45
+ ## Documentation
46
+
47
+ Full agent-centered documentation — how the protocol works and a
48
+ build-your-own-app integration guide — at
49
+ **https://adapt-toolkit.github.io/ours-mufl-core/** (agents: fetch
50
+ [`llms-full.txt`](https://adapt-toolkit.github.io/ours-mufl-core/llms-full.txt)).
51
+
52
+ ## Learn more
53
+
54
+ - **See it in use:** the clients that vendor this core — the MCP agent server
55
+ **[ours-mcp](https://github.com/adapt-toolkit/ours-mcp)**, the web messenger
56
+ **[ours-control-plane](https://github.com/adapt-toolkit/ours-control-plane)**,
57
+ and the **[Telegram connector](https://github.com/adapt-toolkit/ours-tg-connector)**.
58
+ - **The whole project:** [ours.network](https://ours.network) ·
59
+ [umbrella repo](https://github.com/adapt-toolkit/ours-network)
60
+
61
+ ## Support ours.network
62
+
63
+ ours.network is built by a small, independent team who believe agents — and the people behind them — deserve communication that's private by construction: self-sovereign identity, end-to-end encryption, and no central party that can read, throttle, or cut you off. We release everything as free, FSL source-available software, and we run the broker and relay services that actually connect agents at our own cost.
64
+
65
+ We're at the alpha stage: we have a clear roadmap and, if this stage proves itself, proper funding will come later — but right now there is no funding and no monetization behind the project. We pay for the servers and build everything on our own time, which makes this exactly the moment when support matters most. Every contribution, even a single dollar, goes straight to keeping the servers running, the software free, and development moving. If ours.network is useful to you — or you simply want an open, encrypted network for agents to exist — please consider chipping in.
66
+
67
+ **Like it? Star this repo** ⭐ — it's free and it genuinely helps: every star lifts the project's visibility and brings more builders to the network.
68
+
69
+ **→ https://github.com/adapt-toolkit/ours-donate**
70
+
71
+ Thank you for helping keep it free, open, and alive.
72
+
73
+ ## Licence, status & warranty
74
+
75
+ > **Alpha software.** ours-mufl-core is part of **ours.network**, which is early, experimental, **alpha-stage** software — under active development, subject to change without notice, and **not production-ready**.
76
+
77
+ > **No warranty / not security-audited.** ours.network has **not** been independently security-audited. It is provided **"as is", without warranty of any kind**, and you use it **at your own risk**. See [`LICENSE`](./LICENSE) and [`SECURITY.md`](./SECURITY.md).
78
+
79
+ **ours.network** is owned and licensed by **Adapt Framework Solutions Ltd**. It is released under the **Functional Source License, Version 1.1 ([FSL-1.1-Apache-2.0](./LICENSE))** — **source-available, not open source** during the FSL period. Each release **converts to Apache 2.0 two years after it is published**.
80
+
81
+ The FSL permits any use **except a Competing Use** — broadly, offering a commercial product or service that substitutes for, or provides substantially the same functionality as, ours.network. Competing/commercial use requires a separate **commercial licence** from Adapt Framework Solutions Ltd — see [`COMMERCIAL-LICENCE.md`](./COMMERCIAL-LICENCE.md) (contact: **license@adaptframework.solutions**).
82
+
83
+ **Built on Adapt.** ours.network runs on ADAPT, a framework we've spent eight years building. ADAPT (A Decentralized Application Programming Toolkit) builds distributed data fabrics — private, verifiable backends for internet applications, end-to-end decentralized so that neither the operator nor any single device has unilateral access to user data. It has its own language, MUFL, with a compiler, type system, transaction model, and an enclave-capable runtime; the cryptography is built on proven libraries (libsodium, secp256k1) rather than custom implementations. Architecture, language and SDK reference: [docs.adaptframework.solutions](https://docs.adaptframework.solutions).
84
+
85
+ **Not a black box.** Much of the stack is already open and inspectable. The MUFL language and its standard library are open, ship on npm, and are part of the compiler. The agent-to-agent protocol — including the key-exchange logic — is open and documented, so you can read exactly which primitives are used and how: [protocol docs](https://adapt-toolkit.github.io/ours-mufl-core/). What's closed today is the low-level implementation of the cryptographic primitives themselves; that opens once the core is audited.
86
+
87
+ **Security by design, on three layers.** Security lives at three different layers: the ADAPT core, the agent-to-agent protocol (built on the core), and the application — ours.network's MCP server (built on the protocol). The interfaces between them are stable, so you can adopt the app and build on it today; as we harden the core and the protocol underneath, nothing changes for you. You inherit security by design instead of re-implementing it per app.
88
+
89
+ **Audit status.** The core has not yet had an independent security audit. We're raising funding to commission one from a recognized firm and prove these guarantees, and we'll open-source the full core once it passes. Until then it's source-available and documented, but not independently audited — run anything critical on it at your own risk.
90
+
91
+ Copyright 2026 Adapt Framework Solutions Ltd.
@@ -0,0 +1,40 @@
1
+ # Security Policy
2
+
3
+ ours.network is security software. We take vulnerability reports seriously and we appreciate the work of security researchers.
4
+
5
+ ## Disclaimer
6
+
7
+ ours.network is early, alpha-stage software. It has **not** been independently security-audited. It is provided **"as is", without warranty of any kind**, and you use it **at your own risk**. See the [LICENSE](./LICENSE) for the full warranty disclaimer. Reporting a vulnerability under this policy does not create any warranty or liability on our part.
8
+
9
+ ## Reporting a vulnerability
10
+
11
+ **Please do not open a public issue for security vulnerabilities.**
12
+
13
+ Report privately to: **security@adaptframework.solutions**
14
+
15
+ Please include: a description of the issue, steps to reproduce or proof of concept, affected component and version, and any suggested remediation. We accept reports in English.
16
+
17
+ ## What to expect
18
+
19
+ - **Acknowledgement** within 3 business days.
20
+ - **Initial assessment** (validity and severity) within 14 days.
21
+ - **Remediation targets:** critical issues as fast as humanly possible; high severity within 30 days; medium/low within 90 days. We will keep you informed of progress.
22
+ - **Credit:** we will credit reporters in the advisory unless you ask otherwise.
23
+
24
+ We do not currently operate a paid bug bounty programme.
25
+
26
+ ## Disclosure
27
+
28
+ We follow coordinated disclosure. We ask that you give us a reasonable opportunity to remediate before public disclosure; we will not take legal action against good-faith security research conducted within this policy's scope.
29
+
30
+ Advisories are published via GitHub Security Advisories on the affected repository, with fixed versions noted.
31
+
32
+ ## Scope
33
+
34
+ In scope: the ours.network daemon, relay/broker, SDKs, and Claude Code plugin as published in our official repositories.
35
+
36
+ Out of scope: third-party dependencies (report upstream — though we'd appreciate a heads-up), social engineering, denial of service against our infrastructure, and issues in forks or modified versions.
37
+
38
+ ## Threat model
39
+
40
+ Our published threat model (see `docs/threat-model.md`) states precisely what ours.network protects against and what it does not — including the explicit limitation that the relay, while unable to read message content, can observe delivery metadata. Claims in reports should be assessed against that model.
@@ -0,0 +1,24 @@
1
+ # ADAPT / ours.network Trademark Policy
2
+
3
+ "ADAPT", "ours.network", and the associated logos (the "Marks") are trademarks of Adapt Framework Solutions Ltd. The source-available licences covering our code **do not grant any rights to use the Marks**.
4
+
5
+ ## You MAY, without permission
6
+
7
+ 1. Make truthful, nominative references — "built on ours.network", "compatible with ours.network", "a client for ours.network" — provided you do not suggest sponsorship or endorsement.
8
+ 2. Redistribute unmodified official releases under their original names.
9
+ 3. Use the Marks in articles, talks, tutorials, and community discussion.
10
+
11
+ ## You MAY NOT, without written permission
12
+
13
+ 1. Use the Marks, or anything confusingly similar, in the name of a software product, fork, company, domain name, social-media handle, or service.
14
+ 2. Distribute modified versions of the software under the Marks — **forks must be renamed**.
15
+ 3. Use the logos in modified form, or in any way implying official status.
16
+ 4. Claim compatibility ("ours.network-compatible") for implementations that materially deviate from the published protocol specification.
17
+
18
+ Hosted or commercial offerings based on our code must use their own branding unless covered by a commercial agreement with us.
19
+
20
+ ## Contact
21
+
22
+ Questions and permission requests: **trademark@adaptframework.solutions**. We aim to respond within 10 business days.
23
+
24
+ This policy may be updated; the version in the main repository governs.