@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.
- package/LICENSE +98 -0
- package/README.md +231 -0
- package/dist/cli.js +446 -0
- package/dist/config.js +104 -0
- package/dist/daemon.js +353 -0
- package/dist/http.js +31 -0
- package/dist/log-store.js +48 -0
- package/dist/presence.js +157 -0
- package/dist/provision.js +29 -0
- package/dist/push.js +6 -0
- package/dist/web-push-sender.js +41 -0
- package/mufl_code/20E39399689579D5AFA5132C59DEDDD328C3BCA1A74D546F2193B36585DDD283.muflo +0 -0
- package/mufl_code/config.mufl +21 -0
- package/mufl_code/core/.github/FUNDING.yml +3 -0
- package/mufl_code/core/.github/workflows/docs.yml +39 -0
- package/mufl_code/core/.github/workflows/tests.yml +66 -0
- package/mufl_code/core/COMMERCIAL-LICENCE.md +25 -0
- package/mufl_code/core/COMPATIBILITY.md +221 -0
- package/mufl_code/core/CONTRIBUTING.md +37 -0
- package/mufl_code/core/LICENSE +98 -0
- package/mufl_code/core/NOTIFICATIONS_CONTRACT.md +290 -0
- package/mufl_code/core/README.md +91 -0
- package/mufl_code/core/SECURITY.md +40 -0
- package/mufl_code/core/TRADEMARKS.md +24 -0
- package/mufl_code/core/a2a_capabilities.mm +449 -0
- package/mufl_code/core/a2a_cluster.mm +601 -0
- package/mufl_code/core/a2a_control.mm +90 -0
- package/mufl_code/core/a2a_messaging.mm +4817 -0
- package/mufl_code/core/a2a_monitoring.mm +94 -0
- package/mufl_code/core/a2a_notification_integration.mm +144 -0
- package/mufl_code/core/a2a_notifications.mm +1127 -0
- package/mufl_code/core/a2a_protocol.mm +246 -0
- package/mufl_code/core/a2a_versions.mm +801 -0
- package/mufl_code/core/config.mufl +29 -0
- package/mufl_code/core/docs/.vitepress/config.mts +58 -0
- package/mufl_code/core/docs/citations.json +688 -0
- package/mufl_code/core/docs/guide/01-vendor-the-core.md +50 -0
- package/mufl_code/core/docs/guide/02-configure-and-compile.md +100 -0
- package/mufl_code/core/docs/guide/03-wire-the-host.md +154 -0
- package/mufl_code/core/docs/guide/04-connect-and-message.md +156 -0
- package/mufl_code/core/docs/guide/05-test-your-app.md +75 -0
- package/mufl_code/core/docs/guide/index.md +63 -0
- package/mufl_code/core/docs/how-it-works/capabilities-and-control.md +77 -0
- package/mufl_code/core/docs/how-it-works/cluster.md +65 -0
- package/mufl_code/core/docs/how-it-works/identity.md +56 -0
- package/mufl_code/core/docs/how-it-works/invites-and-contacts.md +76 -0
- package/mufl_code/core/docs/how-it-works/messaging.md +55 -0
- package/mufl_code/core/docs/how-it-works/monitoring-and-config.md +72 -0
- package/mufl_code/core/docs/how-it-works/overview.md +45 -0
- package/mufl_code/core/docs/how-it-works/versioning.md +116 -0
- package/mufl_code/core/docs/index.md +27 -0
- package/mufl_code/core/docs/package-lock.json +3467 -0
- package/mufl_code/core/docs/package.json +15 -0
- package/mufl_code/core/docs/reference/contributing.md +26 -0
- package/mufl_code/core/docs/reference/glossary.md +108 -0
- package/mufl_code/core/docs/reference/implementations.md +23 -0
- package/mufl_code/core/docs/reference/modules.md +19 -0
- package/mufl_code/core/docs/scripts/check-citations.mjs +36 -0
- package/mufl_code/core/docs/scripts/check-denylist.mjs +30 -0
- package/mufl_code/core/docs/scripts/gen-llms.mjs +32 -0
- package/mufl_code/core/docs/workflows/cluster.md +89 -0
- package/mufl_code/core/docs/workflows/contact-exchange.md +60 -0
- package/mufl_code/core/docs/workflows/contact-restore.md +60 -0
- package/mufl_code/core/docs/workflows/control-verbs.md +70 -0
- package/mufl_code/core/docs/workflows/index.md +57 -0
- package/mufl_code/core/docs/workflows/introductions.md +60 -0
- package/mufl_code/core/docs/workflows/messaging.md +75 -0
- package/mufl_code/core/docs/workflows/monitoring.md +84 -0
- package/mufl_code/core/docs/workflows/notifications.md +109 -0
- package/mufl_code/core/release-notes/0.1.md +60 -0
- package/mufl_code/core/release-notes/0.3.md +22 -0
- package/mufl_code/core/release-notes/0.4.md +97 -0
- package/mufl_code/core/tests/README.md +83 -0
- package/mufl_code/core/tests/compat/README.md +82 -0
- package/mufl_code/core/tests/compat/compat.mjs +245 -0
- package/mufl_code/core/tests/compat/compat_peer.mjs +146 -0
- package/mufl_code/core/tests/compat/run_compat.sh +96 -0
- package/mufl_code/core/tests/corpus.mjs +139 -0
- package/mufl_code/core/tests/corpus_actor.mu +242 -0
- package/mufl_code/core/tests/fatcompile_actor.mu +52 -0
- package/mufl_code/core/tests/mig.mjs +805 -0
- package/mufl_code/core/tests/mig_actor.mu +518 -0
- package/mufl_code/core/tests/migapp.mjs +431 -0
- package/mufl_code/core/tests/migapp_actor.mu +294 -0
- package/mufl_code/core/tests/mufl_semantics/run.sh +69 -0
- package/mufl_code/core/tests/mufl_semantics/t11_three_version.mufl +47 -0
- package/mufl_code/core/tests/mufl_semantics/t2_extra_fields.mufl +11 -0
- package/mufl_code/core/tests/mufl_semantics/t7_runtime.mufl +30 -0
- package/mufl_code/core/tests/mufl_semantics/t9_passthrough.mufl +19 -0
- package/mufl_code/core/tests/mufl_semantics/t_str_order.mufl +19 -0
- package/mufl_code/core/tests/notif.mjs +755 -0
- package/mufl_code/core/tests/notif_actor.mu +450 -0
- package/mufl_code/core/tests/protocol_container.mm +18 -0
- package/mufl_code/core/tests/run.sh +65 -0
- package/mufl_code/core/tests/run_corpus.sh +51 -0
- package/mufl_code/core/tests/run_fatcompile.sh +40 -0
- package/mufl_code/core/tests/run_mig.sh +51 -0
- package/mufl_code/core/tests/run_migapp.sh +51 -0
- package/mufl_code/core/tests/run_notif.sh +57 -0
- package/mufl_code/core/tests/test.mjs +750 -0
- package/mufl_code/core/tests/test_actor.mu +564 -0
- package/mufl_code/core/tests/test_common.mjs +93 -0
- package/mufl_code/core/version.mm +24 -0
- package/mufl_code/packet.mu +170 -0
- package/mufl_code/protocol_container.mm +18 -0
- package/package.json +43 -0
- 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.
|