@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,601 @@
1
+ // Shared ours cluster library (core.cluster capability handler).
2
+ //
3
+ // Implements CLUSTER_API.md (FROZEN v1 + refinement R1): the core.cluster verb
4
+ // handler dispatched via a2a_capabilities, the root-packet cluster_members
5
+ // registry, the async create/remove workflow (host-primitive notify-actions +
6
+ // host callbacks), and reconcile (host-truth ⨝ registry). The control plane and
7
+ // every root-managing app speak ONE envelope; this library owns the cluster
8
+ // verb logic so no app hand-codes it.
9
+ //
10
+ // Layering: loads a2a_messaging (composes its trns' helper fns: resolve_contact,
11
+ // emit_pair, introduce_name) + a2a_capabilities (build_response, the cap id).
12
+ // The authz chokepoint is in a2a_capabilities::dispatch (RR-9); handlers here
13
+ // run AFTER it, so they may assume the sender is the authorized controller.
14
+ //
15
+ // Response model (R1): a handler RETURNS the response_envelope as transaction
16
+ // return_data (a NATIVE record); the daemon marshals it to JSON + ships via the
17
+ // generic sendControl. Core emits NO send action for responses. MUFL has no JSON
18
+ // codec, so $args (request) and $result/$err (response) are native both ways —
19
+ // the daemon adapts JSON<->native at the boundary, generically.
20
+ library a2a_cluster loads libraries
21
+ current_transaction_info,
22
+ address_document_types,
23
+ a2a_protocol,
24
+ a2a_capabilities,
25
+ a2a_messaging,
26
+ version
27
+ uses transactions
28
+ {
29
+ // ---- host-primitive notify-action event tags (CLUSTER_API.md §7) ----------
30
+ // Emitted to the local daemon (host executor) as $notify_agent actions; never
31
+ // cross the network. The daemon maps each to a runtime op + a host callback.
32
+ ev_provision = "host_provision_child".
33
+ ev_destroy = "host_destroy_child".
34
+ ev_enumerate = "host_enumerate_children".
35
+ // R2: minting a child's invite is irreducibly CROSS-PACKET (only the child's
36
+ // packet can sign its own AD) — so it is the 4th host primitive. The daemon
37
+ // runs generate_invite IN the child's packet and calls back register_child_invite.
38
+ ev_mint_invite = "host_mint_child_invite".
39
+ // Per-child monitoring (single-gate, host-mediated): the daemon binds the CHILD's
40
+ // monitoring_proxy to the root's ceremony-pinned cluster CP (enable: host-run
41
+ // set_proxy_pending+verify_proxy_code on the child; disable: host_clear_child_monitoring).
42
+ ev_set_monitoring = "host_set_child_monitoring".
43
+
44
+ // core 3.2: CP-side inbound name for a pushed roster snapshot (literal — this lib
45
+ // keeps no code dep on a2a_monitoring, which loads us).
46
+ roster_update_tx = "::a2a_monitoring::receive_roster_update".
47
+
48
+ // ---- registry (RR-3: host-truth-backed projection) ------------------------
49
+ // member = the control-plane row for one hosted child. $monitoring in
50
+ // "off"|"pending"|"on"; $caps is the child's advertised capability ids.
51
+ metadef member_t: ($cid -> global_id, $role_id -> str, $name -> str,
52
+ $bio -> str, $persona -> str, $monitoring -> str, $caps -> str[]).
53
+ cluster_members is (global_id ->> member_t) = (,).
54
+
55
+ // A child as enumerated from host truth (RR-4: carries caps + bio so a
56
+ // backfilled member is introduce-capable, not hard-blocked). $child_ad is the
57
+ // child's native address document (the daemon passes it parsed — no in-MUFL
58
+ // deserialization, consistent with R1).
59
+ metadef child_rec_t: ($cid -> global_id, $role_id -> str, $name -> str,
60
+ $bio -> str, $persona -> str, $caps -> str[],
61
+ $child_ad -> address_document_types::t_address_document).
62
+
63
+ // ---- async pending / dedup state (CLUSTER_API.md §8) ----------------------
64
+ // pending_reqs: keyed by the host-unique $pending_handle, holds the full
65
+ // (sender,$req_id) so a callback resolves the right sender (RR-2).
66
+ metadef pending_req_t: ($handle -> str, $sender_id -> global_id, $req_id -> str,
67
+ $verb -> str, $op_key -> str, $bio -> str, $date -> time).
68
+ pending_reqs is (str ->> pending_req_t) = (,).
69
+ // GLOBAL (not per-sender) pending-create op-keys by name (RR-3): blocks a
70
+ // proxy-handoff re-create of a name already mid-provision.
71
+ pending_create_names is (str ->> bool) = (,).
72
+ // Monotonic handle counter (MUFL has no RNG): handle = my_cid + "-" + seq.
73
+ next_handle_seq is int = 0.
74
+ // §8 pending-req TTL: a pending-req older than this is swept (settled) by
75
+ // sweep_and_settle, which WS-B calls AFTER a fresh reconcile (so an adopted
76
+ // orphan is already in the registry before we decide success vs timeout).
77
+ pending_req_ttl_seconds = 120.
78
+
79
+ // ---- local action helpers -------------------------------------------------
80
+ fn _save (_) = (transaction::action::return_data ($kind -> $save_state)).
81
+ fn _ret (payload: any) = (transaction::action::return_data ($kind -> $data, $payload -> payload)).
82
+ fn _notify (payload: any) = (transaction::action::return_data ($kind -> $notify_agent, $payload -> payload)).
83
+
84
+ fn _ok_action (req_id: str, result_val: any) -> transaction::action::type
85
+ {
86
+ return _ret (a2a_capabilities::build_response ($req_id -> req_id, $ok -> TRUE, $result -> result_val, $err -> (,))).
87
+ }
88
+ fn _err_action (req_id: str, code: str, message: str) -> transaction::action::type
89
+ {
90
+ return a2a_capabilities::deny_action ($req_id -> req_id, $code -> code, $message -> message).
91
+ }
92
+
93
+ // ASYNC reply (CLUSTER_API.md §2 R1 routing): a host-fired callback runs at
94
+ // origin::user, so its ctx sender is the DAEMON/self — the async response must
95
+ // route to the ORIGINAL controller stored in the pending-req, never to the
96
+ // callback's own sender. So the callback's return_data is a routing wrapper
97
+ // ($target -> the stored sender cid, $response -> the response_envelope); the
98
+ // daemon ships $response to $target. Routing is always by sender, never by verb.
99
+ fn _async_reply (target: global_id, response: a2a_capabilities::response_envelope_t) -> transaction::action::type
100
+ {
101
+ return _ret ($target -> (_str target), $response -> response).
102
+ }
103
+
104
+ fn _mint_handle (_) -> str
105
+ {
106
+ next_handle_seq -> next_handle_seq + 1.
107
+ return (_str (_get_container_id())) + "-" + (_str next_handle_seq).
108
+ }
109
+
110
+ // member -> the native $result record the daemon marshals to JSON.
111
+ fn _member_view (m: member_t) -> any
112
+ {
113
+ return (
114
+ $cid -> (_str (m $cid)),
115
+ $role_id -> (m $role_id),
116
+ $name -> (m $name),
117
+ $bio -> (m $bio),
118
+ $persona -> (m $persona),
119
+ $monitoring -> (m $monitoring),
120
+ $caps -> (m $caps)
121
+ ).
122
+ }
123
+
124
+ fn _member_views (_) -> any[]
125
+ {
126
+ views is any[] = [].
127
+ sc cluster_members -- ( -> m) { views (_count views|) -> (_member_view m). }
128
+ return views.
129
+ }
130
+
131
+ // core 3.2: build the roster-push actions IFF a control plane is bound. Bumps the
132
+ // seq so each push is ordered; carries the full member view (rosters are small,
133
+ // push-on-change). Returns [] when unbound (push_to_cp_actions also self-gates).
134
+ fn _push_roster_actions (_) -> transaction::action::type[]
135
+ {
136
+ if (a2a_messaging::bound_cp_cid NIL) == NIL { return []. }
137
+ seq = a2a_messaging::next_roster_seq NIL.
138
+ return a2a_messaging::push_to_cp_actions roster_update_tx ($version -> seq, $members -> (_member_views NIL)).
139
+ }
140
+
141
+ // registry lookups for the sweep (the registry is keyed by global_id; create's
142
+ // op-key is the name, remove's is the stringified cid).
143
+ fn _find_by_name (name: str) -> member_t+
144
+ {
145
+ found is member_t+ = NIL.
146
+ sc cluster_members -- ( -> m) ?? found == NIL { if (m $name) == name { found -> m. } }
147
+ return found.
148
+ }
149
+ fn _present_cid_str (cidstr: str) -> bool
150
+ {
151
+ present is bool = FALSE.
152
+ sc cluster_members -- (cid -> _) ?? present == FALSE { if (_str cid) == cidstr { present -> TRUE. } }
153
+ return present.
154
+ }
155
+ fn _timeout_resp (req_id: str, message: str) -> a2a_capabilities::response_envelope_t
156
+ {
157
+ return a2a_capabilities::build_response ($req_id -> req_id, $ok -> FALSE, $result -> (,), $err -> ($code -> "timeout", $message -> message)).
158
+ }
159
+
160
+ // ---- verb handlers (each returns transaction::action::type[]) --------------
161
+ // list: read the registry (post-reconcile). Controller-gated upstream.
162
+ fn _h_list (ctx: any) -> transaction::action::type[]
163
+ {
164
+ return [ _ok_action ((ctx $req_id) safe str) ($members -> (_member_views NIL), $version -> (a2a_messaging::current_roster_seq NIL)) ].
165
+ }
166
+
167
+ // set_bio: registry-only in v1 (§13-Q3). Updates the member's bio.
168
+ fn _h_set_bio (ctx: any) -> transaction::action::type[]
169
+ {
170
+ req_id = (ctx $req_id) safe str.
171
+ args = ctx $args.
172
+ cid = (args $cid) safe global_id.
173
+ m = cluster_members cid.
174
+ if m == NIL { return [ _err_action req_id "not_found" "no such child" ]. }
175
+ bio = (args $bio) safe str.
176
+ cluster_members cid -> ($cid -> (m $cid), $role_id -> (m $role_id), $name -> (m $name),
177
+ $bio -> bio, $persona -> (m $persona), $monitoring -> (m $monitoring), $caps -> (m $caps)).
178
+ out is transaction::action::type[] = [ _ok_action req_id ($cid -> (_str cid), $bio -> bio) ].
179
+ sc (_push_roster_actions NIL) -- ( -> a) { out (_count out|) -> a. }
180
+ out (_count out|) -> _save NIL.
181
+ return out.
182
+ }
183
+
184
+ // set_persona: registry-only, host-authoritative (mirrors set_bio, §13-Q3).
185
+ fn _h_set_persona (ctx: any) -> transaction::action::type[]
186
+ {
187
+ req_id = (ctx $req_id) safe str.
188
+ args = ctx $args.
189
+ cid = (args $cid) safe global_id.
190
+ m = cluster_members cid.
191
+ if m == NIL { return [ _err_action req_id "not_found" "no such child" ]. }
192
+ persona = (args $persona) safe str.
193
+ cluster_members cid -> ($cid -> (m $cid), $role_id -> (m $role_id), $name -> (m $name),
194
+ $bio -> (m $bio), $persona -> persona, $monitoring -> (m $monitoring), $caps -> (m $caps)).
195
+ out is transaction::action::type[] = [ _ok_action req_id ($cid -> (_str cid), $persona -> persona) ].
196
+ sc (_push_roster_actions NIL) -- ( -> a) { out (_count out|) -> a. }
197
+ out (_count out|) -> _save NIL.
198
+ return out.
199
+ }
200
+
201
+ // set_monitoring (ASYNC, single-gate host-mediated, C5): real per-child monitoring.
202
+ // The CP a child is bound to is DERIVED from the root's OWN ceremony-pinned
203
+ // monitoring_proxy (bound_cp_cid) — NEVER from $args (critic load-bearing condition:
204
+ // a child can only ever be bound to the root's ceremonied CP, so no bypass). Emits
205
+ // host_set_child_monitoring; the daemon host-binds (enable) or host-clears (disable)
206
+ // the CHILD's monitoring_proxy, then calls back confirm_child_monitoring. Enable
207
+ // requires the root to be bound to a cluster CP. (e) disable genuinely clears the
208
+ // child's proxy → forwarding stops. (i) child-visible notice = conscious deferred
209
+ // carry (mcp host children are non-human-facing) — see CLUSTER_API.md §9.
210
+ fn _h_set_monitoring (ctx: any) -> transaction::action::type[]
211
+ {
212
+ req_id = (ctx $req_id) safe str.
213
+ cid = ((ctx $args) $cid) safe global_id.
214
+ if (cluster_members cid) == NIL { return [ _err_action req_id "not_found" "no such child" ]. }
215
+ enabled = ((ctx $args) $enabled) safe bool.
216
+ sender_id = (ctx $sender_id) safe global_id.
217
+ date = (ctx $date) safe time.
218
+ handle = _mint_handle NIL.
219
+ pr = ($handle -> handle, $sender_id -> sender_id, $req_id -> req_id,
220
+ $verb -> "set_monitoring", $op_key -> (_str cid), $bio -> "", $date -> date).
221
+ ack = _ret (a2a_capabilities::build_response ($req_id -> req_id, $ok -> TRUE, $result -> ($pending -> TRUE), $err -> (,))).
222
+
223
+ if enabled == TRUE
224
+ {
225
+ // ENABLE: derive the CP from the root's OWN ceremony-pinned proxy, and carry
226
+ // the CP's verified AD so the daemon can HOST-INJECT it into the child as a
227
+ // contact (host_register_monitoring_cp) BEFORE host-running the ceremony — a
228
+ // network introduce is rejected by the child's CP-only acceptance gate and
229
+ // would race the ceremony anyway (§9).
230
+ cp = a2a_messaging::bound_cp_cid NIL.
231
+ if cp == NIL { return [ _err_action req_id "not_bound" "no bound cluster CP to monitor under" ]. }
232
+ cp_ad = a2a_messaging::peer_ads cp?.
233
+ if cp_ad == NIL { return [ _err_action req_id "internal" "bound CP has no stored address document" ]. }
234
+ pending_reqs handle -> pr.
235
+ return [
236
+ _notify ($event -> ev_set_monitoring, $cid -> cid, $cp_cid -> cp?, $cp_ad -> cp_ad?, $enabled -> TRUE, $pending_handle -> handle),
237
+ ack, _save NIL
238
+ ].
239
+ }
240
+ // DISABLE: no CP needed — the daemon host-clears the child's monitoring_proxy.
241
+ pending_reqs handle -> pr.
242
+ return [
243
+ _notify ($event -> ev_set_monitoring, $cid -> cid, $enabled -> FALSE, $pending_handle -> handle),
244
+ ack, _save NIL
245
+ ].
246
+ }
247
+
248
+ // create (async): op-key = name (global). Short-circuit if the name already
249
+ // exists as a member or a pending create (RR-3 registry+pending check; the
250
+ // host-packet (d) check is enforced by the daemon enumerable-before-ack +
251
+ // reconcile). Emits host_provision_child + persists the pending-req.
252
+ fn _h_create (ctx: any) -> transaction::action::type[]
253
+ {
254
+ req_id = (ctx $req_id) safe str.
255
+ args = ctx $args.
256
+ name = (args $name) safe str.
257
+ bio = ((args $bio) == NIL ?? "" ; (args $bio) safe str).
258
+
259
+ dup is bool = FALSE.
260
+ sc cluster_members -- ( -> m) ?? dup == FALSE { if (m $name) == name { dup -> TRUE. } }
261
+ if dup == TRUE || (pending_create_names name) != NIL
262
+ {
263
+ return [ _err_action req_id "duplicate" "a child with that name already exists or is being created" ].
264
+ }
265
+
266
+ handle = _mint_handle NIL.
267
+ pending_reqs handle -> ($handle -> handle, $sender_id -> ((ctx $sender_id) safe global_id), $req_id -> req_id,
268
+ $verb -> "create", $op_key -> name, $bio -> bio, $date -> ((ctx $date) safe time)).
269
+ pending_create_names name -> TRUE.
270
+ return [
271
+ _notify ($event -> ev_provision, $name -> name, $bio -> bio, $pending_handle -> handle),
272
+ _ret (a2a_capabilities::build_response ($req_id -> req_id, $ok -> TRUE, $result -> ($pending -> TRUE), $err -> (,))),
273
+ _save NIL
274
+ ].
275
+ }
276
+
277
+ // remove (async): op-key = cid. Idempotent if already absent. Emits
278
+ // host_destroy_child + pending-req.
279
+ fn _h_remove (ctx: any) -> transaction::action::type[]
280
+ {
281
+ req_id = (ctx $req_id) safe str.
282
+ args = ctx $args.
283
+ cid = (args $cid) safe global_id.
284
+ if (cluster_members cid) == NIL
285
+ {
286
+ return [ _ok_action req_id ($cid -> (_str cid), $removed -> TRUE) ].
287
+ }
288
+ handle = _mint_handle NIL.
289
+ pending_reqs handle -> ($handle -> handle, $sender_id -> ((ctx $sender_id) safe global_id), $req_id -> req_id,
290
+ $verb -> "remove", $op_key -> (_str cid), $bio -> "", $date -> ((ctx $date) safe time)).
291
+ return [
292
+ _notify ($event -> ev_destroy, $cid -> cid, $pending_handle -> handle),
293
+ _ret (a2a_capabilities::build_response ($req_id -> req_id, $ok -> TRUE, $result -> ($pending -> TRUE), $err -> (,))),
294
+ _save NIL
295
+ ].
296
+ }
297
+
298
+ // contact (ASYNC, R2): the controller must become a DIRECT contact of the CHILD
299
+ // (chatWithAgent keys on childCid), so the invite must carry the CHILD's identity
300
+ // — and only the child's packet can sign its own AD. The root handler therefore
301
+ // CANNOT mint it (a root-minted invite = confused deputy → contacts the root).
302
+ // So: validate the cid is a real hosted child (criterion 5), persist a pending-
303
+ // req, and emit host_mint_child_invite; the daemon runs generate_invite IN the
304
+ // child packet and calls back register_child_invite with the child's invite blob.
305
+ // $result.invite shape is UNCHANGED (base64url of an invite bin) — WS-C unaffected.
306
+ fn _h_contact (ctx: any) -> transaction::action::type[]
307
+ {
308
+ req_id = (ctx $req_id) safe str.
309
+ cid = ((ctx $args) $cid) safe global_id.
310
+ if (cluster_members cid) == NIL { return [ _err_action req_id "not_found" "no such child" ]. }
311
+ handle = _mint_handle NIL.
312
+ pending_reqs handle -> ($handle -> handle, $sender_id -> ((ctx $sender_id) safe global_id), $req_id -> req_id,
313
+ $verb -> "contact", $op_key -> (_str cid), $bio -> "", $date -> ((ctx $date) safe time)).
314
+ return [
315
+ _notify ($event -> ev_mint_invite, $cid -> cid, $pending_handle -> handle),
316
+ _ret (a2a_capabilities::build_response ($req_id -> req_id, $ok -> TRUE, $result -> ($pending -> TRUE), $err -> (,))),
317
+ _save NIL
318
+ ].
319
+ }
320
+
321
+ // introduce: compose a2a_messaging::emit_pair (1:1 $peer_a/$peer_b) — both
322
+ // parties must be established contacts (peer_ads). Cluster-scoped child<->contact.
323
+ // resolve_contact aborts on an unknown ref (matches the existing introduce trn);
324
+ // a missing stored AD returns a clean not_found.
325
+ fn _h_introduce (ctx: any) -> transaction::action::type[]
326
+ {
327
+ req_id = (ctx $req_id) safe str.
328
+ args = ctx $args.
329
+ a_id = a2a_messaging::resolve_contact ((args $peer_a) safe str).
330
+ b_id = a2a_messaging::resolve_contact ((args $peer_b) safe str).
331
+ if a_id == b_id { return [ _err_action req_id "bad_args" "cannot introduce a node to itself" ]. }
332
+ ad_a = a2a_messaging::peer_ads a_id.
333
+ ad_b = a2a_messaging::peer_ads b_id.
334
+ if ad_a == NIL || ad_b == NIL { return [ _err_action req_id "not_found" "a peer is not an established contact" ]. }
335
+ actions is transaction::action::type[] =
336
+ a2a_messaging::emit_pair a_id ad_a? (a2a_messaging::introduce_name a_id)
337
+ b_id ad_b? (a2a_messaging::introduce_name b_id).
338
+ actions (_count actions|) -> _ok_action req_id ($ok -> TRUE).
339
+ return actions.
340
+ }
341
+
342
+ // ---- core.monitoring + core.connect handlers (cutover: ALL caps via dispatch) --
343
+ // core.monitoring: bind (auth-class BOOTSTRAP — runs pre-bind; THE envelope-path
344
+ // bootstrap) wraps the shared 6-digit ceremony and on success returns
345
+ // {manifest, members, config} one-shot (WS-C Q3); disable clears the binding.
346
+ fn monitoring_handler (ctx: any) -> transaction::action::type[]
347
+ {
348
+ verb = (ctx $verb) safe str.
349
+ req_id = (ctx $req_id) safe str.
350
+ sender_id = (ctx $sender_id) safe global_id.
351
+ if verb == "bind"
352
+ {
353
+ code = ((ctx $args) $code) safe str.
354
+ r = a2a_messaging::do_verify_proxy_code code sender_id.
355
+ if (r $verified) != TRUE
356
+ {
357
+ return [ _err_action req_id "bind_failed" (r $reason), _save NIL ].
358
+ }
359
+ mf = a2a_capabilities::current_manifest NIL.
360
+ cfg_cap = (mf $capabilities) a2a_capabilities::cap_configuration.
361
+ config_params = ((cfg_cap == NIL) ?? "" ; cfg_cap? $params).
362
+ return [
363
+ _ok_action req_id ($manifest -> mf, $members -> (_member_views NIL), $config -> config_params, $version -> (a2a_messaging::current_roster_seq NIL)),
364
+ _save NIL
365
+ ].
366
+ }
367
+ if verb == "disable"
368
+ {
369
+ if (a2a_messaging::do_disable_monitoring sender_id) != TRUE
370
+ {
371
+ return [ _err_action req_id "not_bound" "sender is not the bound control plane" ].
372
+ }
373
+ return [ _ok_action req_id ($ok -> TRUE), _save NIL ].
374
+ }
375
+ return [ _err_action req_id "unknown_verb" ("core.monitoring has no verb: " + verb) ].
376
+ }
377
+
378
+ // core.connect: introduce (generic peer<->peer; reuses the emit_pair composition).
379
+ fn connect_handler (ctx: any) -> transaction::action::type[]
380
+ {
381
+ verb = (ctx $verb) safe str.
382
+ if verb == "introduce" { return _h_introduce ctx. }
383
+ return [ _err_action ((ctx $req_id) safe str) "unknown_verb" ("core.connect has no verb: " + verb) ].
384
+ }
385
+
386
+ // ---- the capability handler (wired into a2a_capabilities::init $handlers) --
387
+ // Switches on ctx.$verb. Unknown verb -> unknown_verb error (no permissive
388
+ // fall-through, §3). contact/introduce compose a2a_messaging and land next.
389
+ fn cluster_handler (ctx: any) -> transaction::action::type[]
390
+ {
391
+ verb = (ctx $verb) safe str.
392
+ if verb == "list" { return _h_list ctx. }
393
+ if verb == "set_bio" { return _h_set_bio ctx. }
394
+ if verb == "set_persona" { return _h_set_persona ctx. }
395
+ if verb == "set_monitoring" { return _h_set_monitoring ctx. }
396
+ if verb == "create" { return _h_create ctx. }
397
+ if verb == "remove" { return _h_remove ctx. }
398
+ if verb == "contact" { return _h_contact ctx. }
399
+ if verb == "introduce" { return _h_introduce ctx. }
400
+ return [ _err_action ((ctx $req_id) safe str) "unknown_verb" ("core.cluster has no verb: " + verb) ].
401
+ }
402
+
403
+ // ---- host callbacks (CLUSTER_API.md §7) ------------------------------------
404
+ // HOST-ONLY: origin::user (a remote peer is origin::external and cannot claim
405
+ // it) + MUST match an outstanding pending-req by $pending_handle (else abort);
406
+ // the pending-req is consumed atomically on first callback (RR-1 correctness /
407
+ // RR-3 idempotency). Not in any $handlers map, so dispatch cannot reach them.
408
+ trn register_provisioned_child _:($pending_handle -> handle: str,
409
+ $role_id -> role_id: str,
410
+ $child_ad -> child_ad: address_document_types::t_address_document)
411
+ {
412
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
413
+ pr = pending_reqs handle.
414
+ abort "register_provisioned_child: no matching pending-req (unsolicited callback rejected)." when pr == NIL || (pr $verb) != "create".
415
+
416
+ name = pr $op_key.
417
+ cid = child_ad $identity $container_id.
418
+ // C2 (RR-4 on the create path): populate caps so the new child is NOT
419
+ // introduce-hard-blocked (caps=[] reads connectKnown=true,connect=false)
420
+ // for the ≤300s until the next reconcile. Every child of this app
421
+ // advertises the SAME describe() capability set, so seed from my own live
422
+ // manifest; bio from the create's pending-req; role_id from the daemon
423
+ // (it delegated the role). reconcile later refreshes from host truth.
424
+ mf = a2a_capabilities::current_manifest NIL.
425
+ caps_list is str[] = [].
426
+ sc (mf $capabilities) -- (cap_id -> _) { caps_list (_count caps_list|) -> cap_id. }
427
+ cluster_members cid -> ($cid -> cid, $role_id -> role_id, $name -> name, $bio -> (pr $bio), $persona -> "",
428
+ $monitoring -> "off", $caps -> caps_list).
429
+ // consume the pending-req + its global op-key atomically.
430
+ pending_reqs handle -> NIL.
431
+ pending_create_names name -> NIL.
432
+ out is transaction::action::type[] = [
433
+ _async_reply (pr $sender_id) (a2a_capabilities::build_response (
434
+ $req_id -> (pr $req_id), $ok -> TRUE,
435
+ $result -> ($cid -> (_str cid), $name -> name, $monitoring -> "off"),
436
+ $err -> (,)
437
+ ))
438
+ ].
439
+ sc (_push_roster_actions NIL) -- ( -> a) { out (_count out|) -> a. }
440
+ out (_count out|) -> _save NIL.
441
+ return transaction::success out.
442
+ }
443
+
444
+ trn confirm_child_destroyed _:($pending_handle -> handle: str, $cid -> cid: global_id)
445
+ {
446
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
447
+ pr = pending_reqs handle.
448
+ abort "confirm_child_destroyed: no matching pending-req (unsolicited callback rejected)." when pr == NIL || (pr $verb) != "remove".
449
+
450
+ cluster_members cid -> NIL.
451
+ pending_reqs handle -> NIL.
452
+ out is transaction::action::type[] = [
453
+ _async_reply (pr $sender_id) (a2a_capabilities::build_response (
454
+ $req_id -> (pr $req_id), $ok -> TRUE,
455
+ $result -> ($cid -> (_str cid), $removed -> TRUE), $err -> (,)
456
+ ))
457
+ ].
458
+ sc (_push_roster_actions NIL) -- ( -> a) { out (_count out|) -> a. }
459
+ out (_count out|) -> _save NIL.
460
+ return transaction::success out.
461
+ }
462
+
463
+ // register_child_invite (R2 contact callback): the daemon ran generate_invite IN
464
+ // the CHILD's packet (so $invite carries the child's identity/keys/cert — the
465
+ // redeemer reaches the CHILD, not the root) and hands the blob back here. SAME
466
+ // RR-1 guards as the other callbacks: origin::user FIRST + pending-handle match
467
+ // (verb=="contact") + atomic consume — no forged child-invite injection. Routes
468
+ // the invite to the STORED controller via $target (not the daemon/self).
469
+ trn register_child_invite _:($pending_handle -> handle: str, $invite -> invite_blob: bin)
470
+ {
471
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
472
+ pr = pending_reqs handle.
473
+ abort "register_child_invite: no matching pending-req (unsolicited callback rejected)." when pr == NIL || (pr $verb) != "contact".
474
+ pending_reqs handle -> NIL.
475
+ return transaction::success [
476
+ _async_reply (pr $sender_id) (a2a_capabilities::build_response (
477
+ $req_id -> (pr $req_id), $ok -> TRUE,
478
+ $result -> ($invite -> invite_blob), $err -> (,)
479
+ )),
480
+ _save NIL
481
+ ].
482
+ }
483
+
484
+ // confirm_child_monitoring (C5 set_monitoring callback): the daemon host-bound (or
485
+ // host-cleared) the CHILD's monitoring_proxy and reports the resulting state. SAME
486
+ // RR-1 guards (origin::user FIRST + pending-handle match verb==set_monitoring +
487
+ // atomic consume). Updates the registry $monitoring to host truth, routes the reply
488
+ // to the STORED controller via $target.
489
+ trn confirm_child_monitoring _:($pending_handle -> handle: str, $cid -> cid: global_id, $enabled -> enabled: bool)
490
+ {
491
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
492
+ pr = pending_reqs handle.
493
+ abort "confirm_child_monitoring: no matching pending-req (unsolicited callback rejected)." when pr == NIL || (pr $verb) != "set_monitoring".
494
+ pending_reqs handle -> NIL.
495
+ mon = (enabled == TRUE ?? "on" ; "off").
496
+ m = cluster_members cid.
497
+ if m != NIL { cluster_members cid -> ($cid -> (m $cid), $role_id -> (m $role_id), $name -> (m $name), $bio -> (m $bio), $persona -> (m $persona), $monitoring -> mon, $caps -> (m $caps)). }
498
+ out is transaction::action::type[] = [
499
+ _async_reply (pr $sender_id) (a2a_capabilities::build_response (
500
+ $req_id -> (pr $req_id), $ok -> TRUE,
501
+ $result -> ($cid -> (_str cid), $monitoring -> mon), $err -> (,)
502
+ ))
503
+ ].
504
+ sc (_push_roster_actions NIL) -- ( -> a) { out (_count out|) -> a. }
505
+ out (_count out|) -> _save NIL.
506
+ return transaction::success out.
507
+ }
508
+
509
+ // reconcile: host truth ⨝ registry (RR-3). Adds host children missing from the
510
+ // registry (backfills out-of-band/CLI creates + the upgrade migration, RR-5),
511
+ // drops members absent from host truth. $pending_handle="" for timer/boot runs.
512
+ trn reconcile _:($pending_handle -> handle: str, $children -> children: child_rec_t[])
513
+ {
514
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
515
+
516
+ // host cids present this round (for the drop pass).
517
+ seen is (global_id ->> bool) = (,).
518
+ added is int = 0.
519
+ changed is int = 0.
520
+ sc children -- ( -> c)
521
+ {
522
+ cid = c $cid.
523
+ seen cid -> TRUE.
524
+ existing = cluster_members cid.
525
+ if existing == NIL { added -> added + 1. }
526
+ // Preserve CP-authoritative registry fields for an EXISTING member:
527
+ // $monitoring AND $bio (set_bio is registry-only v1, §13-Q3 — host
528
+ // truth must not revert it, C1). For a NEW (backfilled) child, seed
529
+ // both from host truth.
530
+ mon = ((existing == NIL) ?? "off" ; existing? $monitoring).
531
+ bio = ((existing == NIL) ?? (c $bio) ; existing? $bio).
532
+ persona = ((existing == NIL) ?? (c $persona) ; existing? $persona).
533
+ if existing != NIL && ((existing? $name) != (c $name) || (existing? $role_id) != (c $role_id) || (_count (existing? $caps)|) != (_count (c $caps)|)) { changed -> changed + 1. }
534
+ cluster_members cid -> ($cid -> cid, $role_id -> (c $role_id), $name -> (c $name),
535
+ $bio -> bio, $persona -> persona, $monitoring -> mon, $caps -> (c $caps)).
536
+ }
537
+ // drop registry members no longer hosted.
538
+ stale is global_id[] = [].
539
+ sc cluster_members -- (cid -> _) { if (seen cid) == NIL { stale (_count stale|) -> cid. } }
540
+ sc stale -- ( -> cid) { cluster_members cid -> NIL. }
541
+
542
+ // Return a status record (count) so the daemon's mutatingTx await RESOLVES
543
+ // immediately — a save-only trn emits no $data, so the await would block to
544
+ // timeout while holding the root lock (serializing against inbound control,
545
+ // WS-B E2E). $added/$dropped also give the daemon reconcile observability.
546
+ out is transaction::action::type[] = [
547
+ _ret ($ok -> TRUE, $added -> added, $dropped -> (_count stale|), $total -> (_count cluster_members|))
548
+ ].
549
+ if (added > 0 || (_count stale|) > 0 || changed > 0) { sc (_push_roster_actions NIL) -- ( -> a) { out (_count out|) -> a. } }
550
+ out (_count out|) -> _save NIL.
551
+ return transaction::success out.
552
+ }
553
+
554
+ // sweep_and_settle (§8 RR-3/R8-4): WS-B calls this AFTER a fresh reconcile (Option
555
+ // 2), so the registry already reflects host truth. Settles every pending-req aged
556
+ // past pending_req_ttl_seconds: create → adopt if the name now exists (a spawned-
557
+ // but-register-lost child, just adopted by reconcile) else timeout; remove → success
558
+ // if the cid is gone else timeout; contact → timeout (no invite arrived). Returns
559
+ // [($target,$response)…] for the daemon to ship to each STORED controller. C7: an
560
+ // expired create clears BOTH pending_reqs[handle] AND pending_create_names[name], so
561
+ // a lost register can never leave the name permanently un-creatable.
562
+ trn sweep_and_settle _
563
+ {
564
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
565
+ now = (current_transaction_info::get_transaction_time())?.
566
+
567
+ replies is any[] = [].
568
+ expired is str[] = [].
569
+ sc pending_reqs -- (handle -> pr)
570
+ {
571
+ if (_substract_seconds now (pr $date)) > pending_req_ttl_seconds
572
+ {
573
+ expired (_count expired|) -> handle.
574
+ req_id = pr $req_id.
575
+ verb = pr $verb.
576
+ resp is a2a_capabilities::response_envelope_t = (_timeout_resp req_id "request timed out").
577
+ if verb == "create"
578
+ {
579
+ m = _find_by_name (pr $op_key).
580
+ if m != NIL { resp -> a2a_capabilities::build_response ($req_id -> req_id, $ok -> TRUE, $result -> (_member_view m?), $err -> (,)). }
581
+ else { resp -> (_timeout_resp req_id "provision timed out"). }
582
+ }
583
+ if verb == "remove"
584
+ {
585
+ if (_present_cid_str (pr $op_key)) != TRUE { resp -> a2a_capabilities::build_response ($req_id -> req_id, $ok -> TRUE, $result -> ($removed -> TRUE), $err -> (,)). }
586
+ else { resp -> (_timeout_resp req_id "destroy timed out"). }
587
+ }
588
+ if verb == "contact" { resp -> (_timeout_resp req_id "invite mint timed out"). }
589
+ replies (_count replies|) -> ($target -> (_str (pr $sender_id)), $response -> resp).
590
+ }
591
+ }
592
+ // C7: clear expired pending-reqs AND, for creates, their global name op-key.
593
+ sc expired -- ( -> h)
594
+ {
595
+ ph = pending_reqs h.
596
+ if ph != NIL && (ph $verb) == "create" { pending_create_names (ph $op_key) -> NIL. }
597
+ pending_reqs h -> NIL.
598
+ }
599
+ return transaction::success [ _ret ($settled -> replies), _save NIL ].
600
+ }
601
+ }