@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,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
|
+
}
|