@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,449 @@
1
+ // Shared ours capabilities library (core 1.6).
2
+ //
3
+ // Makes CAPABILITIES a first-class protocol property. Every node advertises an
4
+ // app manifest (app_id + name + description + monitoring_status + a TYPED MAP
5
+ // of capabilities), and capability VERBS multiplex over the existing
6
+ // a2a_control transport via a {cap, verb, args, req_id} envelope. This library
7
+ // owns envelope parsing + dispatch: it routes an inbound envelope to the
8
+ // app-injected per-capability handler, so apps stop hand-parsing control JSON.
9
+ // Adding a capability = a new $cap id string — no wire-shape change.
10
+ //
11
+ // Layering: app_id (the cheap "WHO", core 1.5) stays on the a2a_control
12
+ // transport; the manifest is the richer pull (name/description live ONLY here,
13
+ // no double-sourcing). The per-capability $params and verb $args remain opaque
14
+ // JSON strings — that is what a dumb frontend renders (e.g. the config schema +
15
+ // value for core.configuration) — while the ROUTING (cap/verb) is typed.
16
+ //
17
+ // Transport-agnostic by design: dispatch operates on an already-parsed envelope
18
+ // RECORD. The consumer (or its daemon) adapts whatever a2a_control delivers
19
+ // (opaque JSON string today) into that record before calling dispatch.
20
+ library a2a_capabilities loads libraries
21
+ current_transaction_info,
22
+ version
23
+ uses transactions
24
+ {
25
+ // ---- well-known capability ids ---------------------------------------
26
+ // Open id space: "core.*" is reserved for protocol capabilities, apps use
27
+ // "app.*". A new capability is just a new id — no wire change. core.monitoring
28
+ // is governance-required and auto-present on every node (see a2a_monitoring).
29
+ cap_configuration = "core.configuration".
30
+ cap_monitoring = "core.monitoring".
31
+ cap_connect = "core.connect".
32
+ cap_notifications = "core.notifications". // NaaS node surface (a2a_notifications.mm; NOTIFICATIONS_CONTRACT.md) — reserved id, no control verbs in v1.
33
+ // core.cluster (core 2.3): promotes cluster/subagent management from private
34
+ // a2a_messaging plumbing to a FIRST-CLASS, app-reusable capability. It owns
35
+ // child/subagent lifecycle, per-child monitoring authorization, the host-local
36
+ // contact book, and child<->contact + cross-cluster introductions, so ANY
37
+ // root-managing app (messenger today, telegram proxy later) exposes the same
38
+ // generic surface. Verbs stay convention (opaque $verb/$args, see
39
+ // control_envelope_t) — this id only RESERVES the namespace and the contract
40
+ // below blesses it; no wire/type/dispatch change. See CLUSTER_CONTRACT.md.
41
+ //
42
+ // VERB CONTRACT (verb -> reference handler / composition -> current wire alias).
43
+ // Wire aliases are KEPT as-is (no hard rename); core.cluster is the documented
44
+ // umbrella, "app.agents" remains a recognized alias for back-compat. NOTE:
45
+ // CamelCase handlers (provisionIdentity/delegateRole/deleteIdentityCompletely/
46
+ // setAgentMonitoring/connect_sibling/sign_monitoring_auth) are reference-host
47
+ // (ours-mcp) DAEMON composites, NOT single core trns; snake_case names are
48
+ // core a2a_messaging transactions. core.cluster blesses the VERB surface; the
49
+ // consuming app/daemon owns the composition. (Verified vs real handlers, R7.)
50
+ // child.create -> provisionIdentity + set_my_bio + delegateRole + best-effort enroll_delegated_node (alias: create_agent)
51
+ // child.list -> list_contact_roots / list_contacts (cluster filter) (alias: list_agents)
52
+ // child.set_bio -> set_my_bio (child-scoped role label) (alias: update_role)
53
+ // child.remove -> deleteIdentityCompletely — FULL teardown (packet/disk/contact-book/binding) (alias: remove_agent)
54
+ // child.set_monitoring -> setAgentMonitoring = connect_sibling + sign_monitoring_auth(root) + set_monitoring(role) (alias: set_monitoring)
55
+ // contact.list -> list_contacts (host-local book)
56
+ // contact.add -> generate_invite (returns an invite blob the caller redeems) (alias: contact_agent)
57
+ // contact.remove -> remove_contact (host-local book)
58
+ // introduce.child_to_contact -> introduce (1:1 introduction)
59
+ // introduce.cross_cluster -> introduce_to_group (cluster fan-out)
60
+ //
61
+ // GUARDRAILS (critic R6-3 / R6-5 / R7): core.cluster promotes MECHANISMS, NOT
62
+ // POLICY. Policy knobs are APP PARAMS — stated as honest TARGETs, not as
63
+ // already-current behavior:
64
+ // - local_auto_accept: RECOMMENDED safe default = false (introductions queue
65
+ // for operator approval). The reference host ours-mcp currently defaults
66
+ // TRUE (index.ts:1496,1541); flipping to false is a TRACKED OBLIGATION,
67
+ // not yet implemented.
68
+ // - child.set_monitoring child-visible notice: TARGET — authorizing per-child
69
+ // monitoring SHOULD surface a child-visible notice. Current code stores the
70
+ // root-signed monitoring_auth with NO notice; surfacing it is an obligation
71
+ // on the consuming app.
72
+ // - child.remove: DESTRUCTIVE — MUST be operator-confirmed. The reference
73
+ // frontend (messenger AgentNode) implements a confirm() dialog.
74
+ // Tiered authz (observe < manage < create < destroy) is ROADMAP, not yet
75
+ // enforced in core; today the app gates verbs. See CLUSTER_CONTRACT.md.
76
+ // DISCOVERY: advertising core.cluster != having a manageable cluster.
77
+ // describe() is static/shared, so EVERY identity (incl. leaf children with no
78
+ // children) advertises it. A generic consumer (e.g. telegram-proxy) MUST
79
+ // confirm an actual cluster via child.list (empty = none) before offering
80
+ // child.create/child.remove. Benign for messenger (only bound roots show the
81
+ // Cluster tab); a false-positive for capability-presence-keyed UIs.
82
+ cap_cluster = "core.cluster".
83
+ // core 0.7.0 — message receipts (delivery + read), the emit/receive split
84
+ // (COMPATIBILITY.md §receipts). PROTOCOL-surface ids with NO control verbs:
85
+ // they gate receipt traffic (fail-CLOSED on absent caps — emitting to a
86
+ // client that can't parse receipts is the incident class), never authz
87
+ // (REG-6). Delivery-vs-read is the wire $kind, not a capability split.
88
+ cap_receipts_emit = "core.receipts.emit". // "I WILL emit receipts (delivered on arrival, read on my get path)"
89
+ cap_receipts_receive = "core.receipts.receive". // "I consume receipts — send me yours"
90
+
91
+ // core 0.8.0 — end-to-end encryption (Olm double-ratchet signed-message
92
+ // envelope). PROTOCOL-surface id with NO control verbs (cf. core.notifications):
93
+ // "I speak the e2e_signed_message envelope and publish an AD v2 $e2e_bundle."
94
+ // Gates E2E traffic + drives monotonic anti-downgrade (a2a_messaging::e2e_route);
95
+ // never authz (REG-6). Crypto + session state live in the adapt `e2e` library.
96
+ cap_e2e = "core.e2e".
97
+
98
+ // core 0.9.0 — the per-connection E2E migration FSM (offer/ack/commit/confirm
99
+ // + exactly-once bilateral key rotation, a2a_messaging §5). PROTOCOL-surface
100
+ // id with NO control verbs, $advertise-carried: "I run the migration FSM."
101
+ // Gates offer emission (fail-closed + pv self-heal, spec §5.4); never authz
102
+ // (REG-6). SINGLE id, deliberately: migration has NO peer opt-out semantics —
103
+ // a peer that doesn't want it simply never advertises/answers and legacy
104
+ // continues untouched (so the receipts-style id PAIR is not needed here).
105
+ cap_e2e_migrate = "core.e2e.migrate".
106
+
107
+ // core 0.11 — e2e session self-heal (a2a_messaging e2e_rekey_request + unacked
108
+ // redrive). PROTOCOL-surface id with NO control verbs, $advertise-carried:
109
+ // "on a decode failure you may send me e2e_rekey_request and I serve a gated
110
+ // re-key". Gates the REQUESTER's send (a peer that never advertises it gets
111
+ // the readvertise_ad fallback instead — a pre-0.11 daemon must never receive
112
+ // an unknown control tx); never authz (REG-6).
113
+ cap_e2e_rekey = "core.e2e.rekey".
114
+
115
+ // ---- secret-field sentinels (config dialect, core.configuration) ------
116
+ // A secret field's VALUE is never echoed in plaintext: reads carry one of
117
+ // these sentinels, and writes interpret them. "$needs_reentry" is the
118
+ // load-bearing "no new party" guarantee — secrets auto-clear to it on any
119
+ // control-plane change (evict/rebind) so they must be re-entered for the
120
+ // new authority. The schema/value tree itself is opaque JSON in $params;
121
+ // these are the shared constants the daemon (write/store) and frontend
122
+ // (render redacted) agree on.
123
+ secret_set = "$set".
124
+ secret_unset = "$unset".
125
+ secret_needs_reentry = "$needs_reentry".
126
+ // Sentinel for a contact_ref field whose target is not yet a contact:
127
+ // selecting it triggers core.connect for that target (the config <-> connect
128
+ // hinge the telegram connector needs).
129
+ contact_ref_connect = "$connect".
130
+
131
+ // ---- wire shapes ------------------------------------------------------
132
+ // A secret field's redacted state, carried IN PLACE of its value — the value
133
+ // is NEVER present in the manifest. $status is one of the $set / $unset /
134
+ // $needs_reentry sentinels above; $epoch is the control-plane epoch the
135
+ // stored secret was last written under. At manifest build the app derives
136
+ // $status = $needs_reentry whenever $epoch is behind the node's current
137
+ // control-plane epoch (any evict/rebind bumps it), so a secret entered for a
138
+ // prior authority must be re-entered for the new one — the load-bearing
139
+ // "no new party inherits live secrets" guarantee, now in the type system.
140
+ metadef secret_field_t: ($status -> str, $epoch -> int).
141
+ // First-class capability descriptor. $cap is the stable enumerable id (the
142
+ // protocol property a dumb frontend keys off); $version is the per-capability
143
+ // schema version; $params is the opaque per-capability JSON; $secrets is the
144
+ // per-field redacted secret state, keyed by the field's path within $params'
145
+ // schema. $secrets is value-less by construction: $params declares WHICH
146
+ // fields are secret, $secrets carries only their status + epoch, so a secret
147
+ // VALUE can never ride the manifest wire.
148
+ metadef capability_t: ($cap -> str, $version -> int, $params -> str, $secrets -> (str ->> secret_field_t)).
149
+ // The node's self-description. monitoring_status is a first-class manifest
150
+ // field (governance-visible). $version is the manifest envelope version, bumped
151
+ // whenever the manifest content changes so a controller knows to refetch.
152
+ metadef app_manifest_t: (
153
+ $version -> int,
154
+ $app_id -> str,
155
+ $name -> str,
156
+ $description -> str,
157
+ $monitoring_status -> str,
158
+ $capabilities -> (str ->> capability_t)
159
+ ).
160
+ // Capability verb envelope, carried as the a2a_control payload. All fields
161
+ // are plain strings so it round-trips as JSON or as a native record; the
162
+ // per-verb $args stays opaque JSON for the handler to interpret.
163
+ // $args is a NATIVE value (R1): the daemon adapts the inbound JSON payload into
164
+ // this record AND parses $args to native before dispatch — MUFL has no JSON
165
+ // DECODER either, so a string $args would be unreadable by a handler. $cap/$verb/
166
+ // $req_id stay plain strings (routing/correlation only).
167
+ metadef control_envelope_t: ($cap -> str, $verb -> str, $args -> any, $req_id -> str).
168
+ // Response envelope, shipped back as the a2a_control payload and correlated by
169
+ // the controller on (sender,$req_id). $result/$err are NATIVE values (records/
170
+ // arrays/strings), not pre-stringified JSON: MUFL has no JSON encoder, so a
171
+ // handler returns this as transaction return_data and the daemon performs the
172
+ // generic JSON marshalling at the transport boundary (no verb logic in TS).
173
+ // $result present iff $ok; $err = ($code,$message) iff not. See CLUSTER_API.md §2.
174
+ metadef response_envelope_t: ($req_id -> str, $ok -> bool, $result -> any, $err -> any).
175
+
176
+ hidden
177
+ {
178
+ // App-injected: returns the node's CURRENT manifest (secrets already
179
+ // redacted by the config capability). Fires on get_manifest.
180
+ describe is (any -> app_manifest_t) = fn (_: any) -> app_manifest_t {
181
+ abort "describe hook is unset in a2a_capabilities (call a2a_capabilities::init)." when TRUE.
182
+ return ($version -> 0, $app_id -> "", $name -> "", $description -> "", $monitoring_status -> "off", $capabilities -> (,)).
183
+ }
184
+ // App-injected per-capability verb handlers, keyed by $cap id. Each
185
+ // receives ($sender_id, $sender_name, $app_id, $verb, $args, $req_id,
186
+ // $date) and returns the transaction actions (persist / notify / send a
187
+ // response). A handler decides whether to process in-packet or enqueue.
188
+ handlers is (str ->> (any -> transaction::action::type[])) = (,).
189
+ // Fallback for an unknown / unhandled capability id.
190
+ on_unknown is (any -> transaction::action::type[]) = fn (_: any) -> transaction::action::type[] { return []. }
191
+ // App-injected STATEFUL authz gate (RR-9): wired to a2a_messaging::authorize_control
192
+ // (which reads the bound monitoring_proxy — hidden there, a2a_capabilities cannot).
193
+ // Receives ($sender_id,$cap,$verb), returns TRUE iff the sender may run it.
194
+ // NIL until init wires it; dispatch FAIL-FASTS (abort) if a controller-class
195
+ // verb is routed with this unset, so an app can never forget the gate.
196
+ authorizer is (any -> bool)+ = NIL.
197
+ // The static capability-id list captured from init's $supported — the
198
+ // source for the 0.5.0 $caps wire piggyback (self_cap_ids below).
199
+ // DELIBERATELY not derived through describe(): that hook aborts when
200
+ // an app never wires capabilities, and the piggyback must degrade to
201
+ // "advertises nothing" there, never abort an invite/restore leg.
202
+ self_caps is str[] = [].
203
+ // Part C: transport routes stay in this low-level capability namespace,
204
+ // while the messaging layer owns contact state, encryption checks, retry,
205
+ // and migration subscription. Optional for backwards-compatible actors.
206
+ on_advertise is (any -> transaction::results::type) =
207
+ fn (_: any) -> transaction::results::type { return transaction::success []. }.
208
+ on_advertise_ack is (any -> transaction::results::type) =
209
+ fn (_: any) -> transaction::results::type { return transaction::success []. }.
210
+ }
211
+
212
+ // FAIL-FAST: $supported is the static list of capability ids this app
213
+ // implements; init aborts if any lacks a wired handler, so "declared" and
214
+ // "implemented" can never drift. (Distinct from the dynamic describe(),
215
+ // which reads live state at get_manifest time.)
216
+ init = fn (_:(
217
+ $describe -> describe_cb: (any -> app_manifest_t),
218
+ $supported -> supported_caps: str[],
219
+ $handlers -> handler_map: (str ->> (any -> transaction::action::type[])),
220
+ $on_unknown -> fallback: (any -> transaction::action::type[]),
221
+ // RR-9: the stateful authz gate. OPTIONAL for back-compat — an app with no
222
+ // controller-class verbs (e.g. the connector: only core.configuration via
223
+ // its own path) may omit it. If omitted, dispatch fail-fasts on the first
224
+ // controller-class verb, so a cluster app MUST wire it.
225
+ $authorizer -> authorizer_cb: (any -> bool)+,
226
+ // core 0.7.0, OPTIONAL (absent from pre-0.7 callers): PROTOCOL-surface
227
+ // capability ids advertised on the wire piggyback WITHOUT control-verb
228
+ // handlers (e.g. core.receipts.*). $supported keeps its declared-implies-
229
+ // implemented handler guard; $advertise is for ids that gate peer
230
+ // traffic shaping only and never route through dispatch.
231
+ $advertise -> advertise_list: str[]+,
232
+ $on_advertise -> advertise_cb: (any -> transaction::results::type)+,
233
+ $on_advertise_ack -> advertise_ack_cb: (any -> transaction::results::type)+
234
+ ))
235
+ {
236
+ describe -> describe_cb.
237
+ handlers -> handler_map.
238
+ on_unknown -> fallback.
239
+ if authorizer_cb != NIL { authorizer -> authorizer_cb. }
240
+ if advertise_cb != NIL { on_advertise -> advertise_cb?. }
241
+ if advertise_ack_cb != NIL { on_advertise_ack -> advertise_ack_cb?. }
242
+ merged is str[] = [].
243
+ sc supported_caps -- ( -> cap) { merged (_count merged|) -> cap. }
244
+ if advertise_list != NIL
245
+ {
246
+ sc advertise_list? -- ( -> cap) { merged (_count merged|) -> cap. }
247
+ }
248
+ self_caps -> merged.
249
+ sc supported_caps -- ( -> cap)
250
+ {
251
+ abort "Capability declared without a handler: " + cap when (handler_map cap) == NIL.
252
+ }
253
+ }
254
+
255
+ // Does THIS node advertise `cap` on the wire piggyback ($supported ∪
256
+ // $advertise, captured at init)? Empty/uninited apps advertise nothing —
257
+ // degrade, never abort (the self_cap_ids contract).
258
+ fn self_advertises (cap: str) -> bool
259
+ {
260
+ found is bool = FALSE.
261
+ sc self_caps -- ( -> c)
262
+ {
263
+ if c == cap { found -> TRUE. break. }
264
+ }
265
+ return found.
266
+ }
267
+
268
+ // The capability ids THIS node advertises on the 0.5.0 wire piggyback
269
+ // ($caps in the invite/restore identity bundles, SPEC §4). Empty for an
270
+ // app that never ran init — degrade, never abort (CAP-1 spirit).
271
+ fn self_cap_ids (_) -> str[]
272
+ {
273
+ return self_caps.
274
+ }
275
+
276
+ // Append a PROTOCOL-surface ($advertise-class) capability id to the live self_caps at RUNTIME —
277
+ // e.g. enable migration mid-session (cap_e2e_migrate) WITHOUT a restart, so the existing e2e
278
+ // session is preserved (a restart re-keys the Olm ratchet). Idempotent (no duplicate). The next
279
+ // outbound message's self_cap_ids piggyback carries it, so the peer re-learns and its
280
+ // mig_should_trigger fires. Intended for $advertise-class ids (traffic-shaping, no control verb) —
281
+ // NOT $supported ids (those require a wired handler, still enforced at init).
282
+ fn add_self_cap (cap: str) -> nil
283
+ {
284
+ if (self_advertises cap) != TRUE { self_caps (_count self_caps|) -> cap. }
285
+ }
286
+
287
+ // Thin, callback-routed wire transactions. a2a_capabilities intentionally
288
+ // owns no contact or retry state; consumers register the orchestration hooks.
289
+ trn advertise args: any { return on_advertise args. }
290
+ trn advertise_ack args: any { return on_advertise_ack args. }
291
+
292
+ // ---- manifest helpers -------------------------------------------------
293
+ fn has_capability (_:($manifest -> m: app_manifest_t, $cap -> cap: str)) -> bool
294
+ {
295
+ return (m $capabilities cap) != NIL.
296
+ }
297
+
298
+ fn get_capability (_:($manifest -> m: app_manifest_t, $cap -> cap: str)) -> capability_t+
299
+ {
300
+ return m $capabilities cap.
301
+ }
302
+
303
+ // The node's current live manifest (reads the app-injected describe hook).
304
+ // Public fn accessor so a handler library (e.g. a2a_cluster's bind handler,
305
+ // which returns {manifest,members,config}) can read it without the readonly
306
+ // get_manifest trn (handlers run inside a trn and call fns, not trns).
307
+ fn current_manifest (_) -> app_manifest_t
308
+ {
309
+ return describe NIL.
310
+ }
311
+
312
+ // Does THIS node's own live manifest advertise `cap`? Reads the app-injected
313
+ // describe() hook (live state at call time) so another library — e.g.
314
+ // a2a_messaging's ingest_connect_descriptor node-side gate — can enforce
315
+ // "I only accept introductions if I actually support core.connect" without
316
+ // reaching into the hidden describe field. describe is wired by init, which
317
+ // every packet runs before serving, so it is set whenever this is reached.
318
+ fn self_supports (cap: str) -> bool
319
+ {
320
+ return has_capability ($manifest -> describe NIL, $cap -> cap).
321
+ }
322
+
323
+ // ---- authorization policy (pure) + response builder -------------------
324
+ // PURE policy table for the single pre-dispatch authz chokepoint
325
+ // (CLUSTER_API.md §3). Returns the auth CLASS a (cap,verb) requires; it reads
326
+ // NO state, so it lives here in the lowest layer. The STATEFUL gate
327
+ // a2a_messaging::authorize_control combines this with the bound-proxy identity
328
+ // (monitoring_proxy is hidden in a2a_messaging, which loads THIS library —
329
+ // the dependency runs that way, not the reverse). FAIL-CLOSED: anything not
330
+ // explicitly public/bootstrap returns "controller", so an unknown or typo'd
331
+ // cap/verb is denied to strangers at the chokepoint before dispatch.
332
+ // "public" — no auth; the get_manifest readonly trn only (no member data).
333
+ // "bootstrap" — core.monitoring.bind only; auth is 6-digit code possession.
334
+ // "controller" — the bound control proxy; the explicit verbs below.
335
+ // "deny" — anything not listed (RR-6 deny-all): a new verb must be
336
+ // consciously classified here to become reachable.
337
+ fn control_auth_class (_:($cap -> cap: str, $verb -> verb: str)) -> str
338
+ {
339
+ // NOTE (C6): get_manifest is NOT classified here — it is a standalone
340
+ // `trn readonly` (get_manifest below), never routed through dispatch, so it
341
+ // never reaches this table. The CLUSTER_API.md §3 "public" row documents the
342
+ // get_manifest trn itself, not a (cap,verb) envelope.
343
+ if cap == cap_monitoring && verb == "bind" { return "bootstrap". }
344
+ if cap == cap_cluster && (verb == "list" || verb == "create" || verb == "set_bio"
345
+ || verb == "set_persona"
346
+ || verb == "remove" || verb == "set_monitoring" || verb == "contact"
347
+ || verb == "introduce") { return "controller". }
348
+ if cap == cap_monitoring && verb == "disable" { return "controller". }
349
+ if cap == cap_connect && verb == "introduce" { return "controller". }
350
+ if cap == cap_configuration && (verb == "get_config" || verb == "set_config") { return "controller". }
351
+ return "deny".
352
+ }
353
+
354
+ // Build a response envelope (CLUSTER_API.md §2). $result/$err are NATIVE values
355
+ // the handler assembles per verb; it is carried back as transaction return_data
356
+ // and the daemon marshals it to JSON + ships — no in-MUFL JSON, no core send.
357
+ fn build_response (_:(
358
+ $req_id -> req_id: str, $ok -> ok: bool, $result -> result_val: any, $err -> err_val: any
359
+ )) -> response_envelope_t
360
+ {
361
+ return ($req_id -> req_id, $ok -> ok, $result -> result_val, $err -> err_val).
362
+ }
363
+
364
+ // Build the return_data action carrying an error response_envelope (the daemon
365
+ // marshals + ships it). Used by the dispatch authz chokepoint and handlers.
366
+ fn deny_action (_:($req_id -> req_id: str, $code -> code: str, $message -> message: str)) -> transaction::action::type
367
+ {
368
+ return transaction::action::return_data (
369
+ $kind -> $data,
370
+ $payload -> build_response (
371
+ $req_id -> req_id, $ok -> FALSE,
372
+ $result -> (,), $err -> ($code -> code, $message -> message)
373
+ )
374
+ ).
375
+ }
376
+
377
+ // ---- dispatch ---------------------------------------------------------
378
+ // Route a parsed capability envelope to its handler. The consumer wires this
379
+ // into a2a_control's on_control_received (after adapting the opaque payload
380
+ // into an envelope record). Unknown caps fall through to on_unknown.
381
+ fn dispatch (_:(
382
+ $sender_id -> sender_id: global_id,
383
+ $sender_name -> sender_name: str,
384
+ $app_id -> app_id: str,
385
+ $envelope -> env: any,
386
+ $date -> date: time
387
+ )) -> transaction::action::type[]
388
+ {
389
+ cap = (env $cap) safe str.
390
+ verb = (env $verb) safe str.
391
+ args = env $args. // NATIVE (R1), handler interprets per verb
392
+ req_id = ((env $req_id) == NIL ?? "" ; (env $req_id) safe str).
393
+
394
+ // ---- RR-9 pre-route authz chokepoint (non-bypassable BY CONSTRUCTION) ----
395
+ // The gate lives HERE, inside dispatch, so an app cannot wire routing while
396
+ // forgetting the gate. deny/unauthorized are returned as a response_envelope
397
+ // via return_data (the daemon marshals+ships); no in-MUFL JSON, no core send.
398
+ klass = control_auth_class ($cap -> cap, $verb -> verb).
399
+ if klass == "deny"
400
+ {
401
+ return [ deny_action ($req_id -> req_id, $code -> "unknown_verb", $message -> "verb not permitted: " + cap + "/" + verb) ].
402
+ }
403
+ if klass == "controller"
404
+ {
405
+ // FAIL-FAST (mirrors init's missing-handler abort): a controller verb must
406
+ // never route without the stateful gate wired.
407
+ abort "a2a_capabilities::dispatch routed a controller verb without an authorizer wired (pass $authorizer to init)." when authorizer == NIL.
408
+ if (authorizer? ($sender_id -> sender_id, $cap -> cap, $verb -> verb)) != TRUE
409
+ {
410
+ return [ deny_action ($req_id -> req_id, $code -> "unauthorized", $message -> "sender is not the bound controller") ].
411
+ }
412
+ }
413
+ // "public" + "bootstrap" fall through (bootstrap's own check is in the bind handler).
414
+
415
+ ctx = (
416
+ $sender_id -> sender_id,
417
+ $sender_name -> sender_name,
418
+ $app_id -> app_id,
419
+ $verb -> verb,
420
+ $args -> args,
421
+ $req_id -> req_id,
422
+ $date -> date
423
+ ).
424
+
425
+ handler = handlers cap.
426
+ if handler == NIL
427
+ {
428
+ return on_unknown (
429
+ $sender_id -> sender_id,
430
+ $sender_name -> sender_name,
431
+ $app_id -> app_id,
432
+ $cap -> cap,
433
+ $verb -> verb,
434
+ $args -> args,
435
+ $req_id -> req_id,
436
+ $date -> date
437
+ ).
438
+ }
439
+ return handler? ctx.
440
+ }
441
+
442
+ // ---- manifest read ----------------------------------------------------
443
+ // Pull the current manifest (for a controller / connecting peer). Readonly:
444
+ // no state change. Secrets are already redacted by the app's describe().
445
+ trn readonly get_manifest _
446
+ {
447
+ return describe NIL.
448
+ }
449
+ }