@adapt-toolkit/a2adapt 0.11.3 → 0.11.5

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.
@@ -5,20 +5,39 @@
5
5
  // encrypted; the key exchange is handled for us by the stdlib `encrypted_channel`
6
6
  // library — we only ever address peers by their container id.
7
7
  //
8
- // The wire-facing shapes and shared verification logic live in the shared
9
- // a2adapt-mufl-core repo (checked out as the core/ subfolder of this
10
- // directory): libraries `a2a_protocol` and `version`. They are shared with
8
+ // The shared transaction logic lives in the a2adapt-mufl-core repo (checked
9
+ // out as the core/ subfolder of this directory): `a2a_protocol` (wire shapes +
10
+ // verification), `a2a_messaging` (contact + message wire path, addressed
11
+ // ::a2a_messaging::<name> by the host), and `version`. They are shared with
11
12
  // the web messenger — change them there, bump core_version, and recompile
12
13
  // every consumer.
13
14
  //
15
+ // This packet keeps only what is agent-specific:
16
+ // - the inbox message store with its lifecycle (unread -> processed ->
17
+ // ready_to_delete -> deleted; see get_messages / gc / defer_messages)
18
+ // - the local contact book + identity hierarchy transactions (host-fired)
19
+ // - export_state / import_state wrappers composing the core helpers with
20
+ // the app-side fields (including the legacy-blob migrations)
21
+ // - ::actor:: compat shims for the network-visible inbound transactions
22
+ // (accept_contact, receive_message) — pre-migration peers send to these
23
+ // names, and the core keeps SENDING to them too (Option A: zero network
24
+ // break; drop both only when no old clients remain)
25
+ //
26
+ // Storage is wired into the core via a2a_messaging::init hooks (see the
27
+ // hidden block): on_message_received deposits into the inbox (or the
28
+ // pending-introduction queue for unknown senders); send/remove hooks are
29
+ // agent no-ops.
30
+ //
14
31
  // User transactions (each backs one MCP tool, except gc which the host fires):
15
- // set_my_name — set the display name peers see for me
16
- // set_my_bio — set my profile bio (free-text, self-asserted)
17
- // generate_invite — make a slim personal invite blob for a named peer
18
- // add_contact — join via an invite blob, reply to the inviter
19
- // send_message — send an e2e-encrypted message to a contact
20
- // remove_contact — forget a contact (drops it from my contacts)
21
- // list_contacts — (readonly) my contacts
32
+ // ::a2a_messaging::set_my_name — set the display name peers see for me
33
+ // ::a2a_messaging::set_my_bio — set my profile bio (free-text, self-asserted)
34
+ // ::a2a_messaging::generate_invite — make an invite blob for a named peer
35
+ // ::a2a_messaging::add_contact — join via an invite blob, reply to the inviter
36
+ // ::a2a_messaging::send_message — send an e2e-encrypted message to a contact
37
+ // ::a2a_messaging::remove_contact — forget a contact
38
+ // ::a2a_messaging::list_contacts — (readonly) my contacts
39
+ // ::a2a_messaging::list_contact_roots — (readonly) verified root linkage per contact
40
+ // ::a2a_messaging::get_version — (readonly) shared-core version
22
41
  // list_incoming_messages — (readonly) my inbox, with per-message id + status
23
42
  // get_messages — return unread messages + mark them processed (sole body egress)
24
43
  // defer_messages — flip processed/ready_to_delete messages back to unread
@@ -29,7 +48,6 @@
29
48
  // export_root_profile — root-only in practice: my self-signed root profile blob
30
49
  // set_delegation — role: store my verified delegation cert + root material
31
50
  // describe_identity — (readonly) name/bio/hierarchy position
32
- // list_contact_roots — (readonly) verified root linkage per contact
33
51
  // connect_sibling — register an intra-root peer + send sibling_introduce
34
52
  //
35
53
  // Local contact book (host-fired transactions; see the design notes above
@@ -44,18 +62,25 @@
44
62
  // reject_introduction — drop a pending local introduction
45
63
  // list_pending_introductions — (readonly) pending introductions (names + queue sizes)
46
64
  //
65
+ // Monitoring + control plane (host-fired unless noted; see
66
+ // MONITORING-AND-SHARED-LIBRARY-DESIGN.md):
67
+ // get_monitoring_status — (readonly) enabled flag / proxy binding / queue sizes
68
+ // sign_monitoring_auth — root-only: sign an enable/disable auth for a role
69
+ // set_monitoring — role: verify the root-signed auth, set the flag
70
+ // get_monitoring_copies — root: drain queued copies for proxy forwarding
71
+ // get_control_requests — root: drain queued proxy control requests
72
+ // set_proxy_pending — root: store the host-generated 6-digit code
73
+ // verify_proxy_code — root: check a bind attempt (atomic attempts/expiry)
74
+ // clear_monitoring_proxy — root: drop the proxy binding
75
+ // ::a2a_control::send_control — send an opaque control payload to a contact
76
+ //
47
77
  // External transactions (inbound, not exposed as tools):
48
- // accept_contact — inviter learns the joiner's identity + name
49
- // receive_message — store a decrypted inbound message
78
+ // accept_contact — inviter learns the joiner's identity + name (core shim)
79
+ // receive_message — store a decrypted inbound message (core shim)
80
+ // receive_monitoring_copy — a monitored role of mine reports a message copy
81
+ // ::a2a_control::control_message — control payload from a contact (queued for the daemon)
50
82
  // local_introduce — same-host peer connects via the local contact book
51
83
  // sibling_introduce — intra-root peer connects, authorized by its delegation cert
52
- //
53
- // Naming model (personal invites): generate_invite('Bob') tags a pending invite
54
- // with the peer-name "Bob"; whoever redeems it is registered under "Bob" (the
55
- // inviter's assigned name wins). Generating WITHOUT a name tags the invite with
56
- // "" — then the joiner's self-announced name wins (falling back to its container
57
- // id if the joiner never set one). The joiner, in turn, sees the inviter under
58
- // the inviter's own self-name (set via set_my_name), unless the joiner overrides it.
59
84
 
60
85
  application actor loads libraries
61
86
  identity_proof_document,
@@ -70,6 +95,8 @@ application actor loads libraries
70
95
  encrypted_channel,
71
96
  current_transaction_info,
72
97
  a2a_protocol,
98
+ a2a_messaging,
99
+ a2a_control,
73
100
  version
74
101
  uses transactions
75
102
  {
@@ -101,33 +128,54 @@ application actor loads libraries
101
128
  seen_nonce_cap is int = 1024.
102
129
  pending_queue_cap is int = 50.
103
130
 
131
+ // ---- monitoring shapes + limits (see MONITORING-AND-SHARED-LIBRARY-DESIGN.md) ----
132
+ // Root-signed authorization for flipping a role's monitoring flag. Like
133
+ // a delegation cert, verified against the role's pinned root keys, so
134
+ // only this hierarchy's root can change what its roles report.
135
+ metadef monitoring_auth_core_t: ($version -> int, $role_cid -> global_id, $enabled -> bool, $issued_at -> time).
136
+ metadef monitoring_auth_t: ($c -> monitoring_auth_core_t, $s -> crypto_signature).
137
+ // One monitored message, as the role reports it to its root.
138
+ metadef monitoring_copy_t: (
139
+ $version -> int,
140
+ $source_cid -> global_id,
141
+ $source_name -> str,
142
+ $direction -> str,
143
+ $peer_cid -> global_id,
144
+ $peer_name -> str,
145
+ $date -> time,
146
+ $body -> str
147
+ ).
148
+ // Proxy binding (root only): a pending 6-digit-code verification and
149
+ // the verified binding that replaces it on success.
150
+ metadef proxy_pending_t: ($code -> str, $proxy_cid -> global_id, $created_at -> time, $attempts -> int).
151
+ metadef proxy_binding_t: ($proxy_cid -> global_id, $bound_at -> time).
152
+ // One queued control request from the bound browser proxy.
153
+ metadef control_req_t: ($sender_cid -> global_id, $sender_name -> str, $payload -> str, $date -> time).
154
+
155
+ proxy_code_max_age_seconds is int = 300.
156
+ proxy_max_attempts is int = 3.
157
+ monitoring_inbox_cap is int = 500.
158
+ control_inbox_cap is int = 200.
159
+
104
160
  // Wire the deserialization primitive into the libraries that need it.
105
161
  _read_or_abort = grab( _read_or_abort ).
106
162
  key_storage::init ($_read_or_abort -> _read_or_abort).
107
163
  encrypted_channel::init ($_read_or_abort -> _read_or_abort).
108
164
 
109
165
  // ---- packet state ---------------------------------------------------
110
- // The display name peers see for me (set via set_my_name).
111
- my_name is str = "".
112
- // Known contacts, keyed by their container id.
113
- contacts is (global_id ->> a2a_protocol::contact_t) = (,).
114
- // Invites I generated, keyed by invite id -> the name I assigned the peer.
115
- pending_invites is (global_id ->> str) = (,).
166
+ // (The shared contact/profile/hierarchy state my_name, contacts,
167
+ // pending_invites, peer_ads, my_bio, delegation_cert, root_ad,
168
+ // root_profile, contact_roots lives in a2a_messaging and is read /
169
+ // assigned as a2a_messaging::<field> below.)
170
+ //
116
171
  // Received messages. Each carries its own lifecycle status (see
117
- // message_t / get_messages / mark_processed / defer_messages), so the
172
+ // message_t / get_messages / gc / defer_messages), so the
118
173
  // packet is the single authority on what has been read or processed —
119
174
  // no host-side cursor, safe across concurrent sessions.
120
175
  inbox is message_t[] = [].
121
176
  // Monotonic source of per-message ids (the stable handle the agent uses
122
177
  // to mark a message processed or defer it).
123
178
  next_msg_seq is int = 1.
124
- // Peer address documents, captured when a contact is established. These are
125
- // self-signed, code-independent, and seed-stable, so on a code upgrade the
126
- // host re-creates this packet from the same seed and import_state replays
127
- // them through address_document::process_address_document — re-registering
128
- // every peer's keys in key_storage so encrypted channels survive the upgrade
129
- // with no re-handshake. Only peer PUBLIC keys travel here, never secrets.
130
- peer_ads is (global_id ->> address_document_types::t_address_document) = (,).
131
179
  // The host registrar's address document (pinned once at identity
132
180
  // creation / injected on upgrade) — its $identity $key_list is what
133
181
  // introduction credentials are verified against. NIL means this identity
@@ -142,18 +190,23 @@ application actor loads libraries
142
190
  // Verified-but-unapproved local introductions (when auto-accept is off),
143
191
  // each with a bounded queue of messages awaiting approval.
144
192
  pending_introductions is (global_id ->> pending_intro_t) = (,).
145
- // ---- identity hierarchy state ----
146
- // My profile bio (free-text, self-asserted; carried in role invites).
147
- my_bio is str = "".
148
- // My delegation cert. NIL == I am a root or a legacy flat identity.
149
- delegation_cert is a2a_protocol::delegation_cert_t+ = NIL.
150
- // My root's address document (set with the cert; its key list is what
151
- // sibling introductions and my own cert are verified against).
152
- root_ad is address_document_types::t_address_document+ = NIL.
153
- // My root's self-signed profile, embedded in the invites I generate.
154
- root_profile is a2a_protocol::root_profile_t+ = NIL.
155
- // Verified root linkage per contact, keyed by the contact's container id.
156
- contact_roots is (global_id ->> a2a_protocol::contact_root_t) = (,).
193
+
194
+ // ---- monitoring state -------------------------------------------------
195
+ // Whether THIS packet (a role) reports its message traffic to its root.
196
+ // Only flipped by a root-signed authorization (set_monitoring).
197
+ monitoring_enabled is bool = FALSE.
198
+ // Root only: copies received from monitored roles, awaiting the host's
199
+ // get_monitoring_copies pull (forwarded to the bound proxy). Capped;
200
+ // oldest copies are dropped first when no proxy drains the queue.
201
+ monitoring_inbox is monitoring_copy_t[] = [].
202
+ // Root only: the in-flight proxy binding (6-digit code verification)
203
+ // and the verified proxy that monitoring traffic is forwarded to.
204
+ proxy_pending is proxy_pending_t+ = NIL.
205
+ monitoring_proxy is proxy_binding_t+ = NIL.
206
+ // Root only: control requests from the bound proxy, awaiting the
207
+ // host's get_control_requests pull. Kept out of the message inbox so
208
+ // agent sessions never see them.
209
+ control_inbox is control_req_t[] = [].
157
210
 
158
211
  // Signal the host to persist the packet. Only emitted at the end of a
159
212
  // complete procedure — intermediate states (e.g. channel handshake) are
@@ -162,19 +215,6 @@ application actor loads libraries
162
215
  fn _return_data (payload: any) = (transaction::action::return_data ($kind -> $data, $payload -> payload)).
163
216
  fn _notify_agent (payload: any) = (transaction::action::return_data ($kind -> $notify_agent, $payload -> payload)).
164
217
 
165
- // Resolve a contact reference (a display name or stringified container id)
166
- // to a container id; aborts if no contact matches.
167
- fn resolve_contact (ref: str) -> global_id
168
- {
169
- found is global_id+ = NIL.
170
- sc contacts -- (cid -> c) ?? found == NIL && ((c $name) == ref || (_str cid) == ref)
171
- {
172
- found -> cid.
173
- }
174
- abort "Unknown contact: " + ref when found == NIL.
175
- return found?.
176
- }
177
-
178
218
  // Append a message to the inbox under a fresh id; returns the id.
179
219
  fn deposit_message (sender_id: global_id, sender_name: str, text: str, msg_date: time) -> int
180
220
  {
@@ -191,6 +231,39 @@ application actor loads libraries
191
231
  return mid.
192
232
  }
193
233
 
234
+ // Build the monitoring-copy action for one message IF this packet is a
235
+ // monitored role with a live encrypted channel to its root; [] otherwise.
236
+ // The is_container_registered guard makes a missing/lost root channel
237
+ // degrade to "no copy" instead of failing the user's message — the
238
+ // enable flow (host-side) establishes the channel via connect_sibling.
239
+ fn monitor_copy_actions (direction: str, peer_cid: global_id, text: str, msg_date: time) -> transaction::action::type[]
240
+ {
241
+ if monitoring_enabled == FALSE || a2a_messaging::delegation_cert == NIL { return []. }
242
+ root_cid = a2a_messaging::delegation_cert? $c $root_cid.
243
+ if key_storage::is_container_registered(root_cid) != TRUE { return []. }
244
+
245
+ peer_name is str = "".
246
+ p = a2a_messaging::contacts peer_cid.
247
+ if p != NIL { peer_name -> p? $name. }
248
+
249
+ copy is monitoring_copy_t = (
250
+ $version -> 1,
251
+ $source_cid -> _get_container_id(),
252
+ $source_name -> a2a_messaging::my_name,
253
+ $direction -> direction,
254
+ $peer_cid -> peer_cid,
255
+ $peer_name -> peer_name,
256
+ $date -> msg_date,
257
+ $body -> text
258
+ ).
259
+ return [
260
+ encrypted_channel::send_encrypted_tx root_cid (
261
+ $name -> "::actor::receive_monitoring_copy",
262
+ $targ -> ($copy -> copy)
263
+ )
264
+ ].
265
+ }
266
+
194
267
  // Resolve a pending introduction by joiner name or stringified container
195
268
  // id; aborts when nothing matches.
196
269
  fn resolve_pending (ref: str) -> global_id
@@ -203,229 +276,83 @@ application actor loads libraries
203
276
  abort "No pending introduction matches: " + ref when found == NIL.
204
277
  return found?.
205
278
  }
206
- }
207
-
208
- // ---- user transactions --------------------------------------------------
209
-
210
- trn set_my_name _:($name -> name: str)
211
- {
212
- current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
213
- my_name -> name.
214
- return transaction::success [
215
- _return_data ($name -> name),
216
- _save_state NIL
217
- ].
218
- }
219
279
 
220
- trn set_my_bio _:($bio -> bio: str)
221
- {
222
- current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
223
- my_bio -> bio.
224
- return transaction::success [
225
- _return_data ($bio -> bio),
226
- _save_state NIL
227
- ].
228
- }
229
-
230
- trn generate_invite _:($name -> name: str+)
231
- {
232
- current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
233
-
234
- invite_id = _new_id "a2adapt invite".
235
- // Remember the name I'm assigning to whoever redeems this invite. An
236
- // empty string is the "no assigned name" sentinel: accept_contact then
237
- // registers the joiner under its self-announced name instead.
238
- assigned = (name == NIL ?? "" ; name?).
239
- pending_invites invite_id -> assigned.
240
-
241
- // Carry only my signed identity (public keys) + its self-signatures. The
242
- // joiner rebuilds my address document from these (version is constant, my
243
- // container_id is the identity's own field) and stores it so it can
244
- // re-register me after a code upgrade (see peer_ads / import_state). The
245
- // identity sub-record is passed through untouched so its _value_id what
246
- // the authorizations sign over — survives the round trip unchanged.
247
- my_ad = address_document::get_my_address_document().
248
- my_identity = my_ad $identity.
249
-
250
- // A delegated role embeds its cert + the root's profile (Ring 3 of the
251
- // identity hierarchy: external peers verify the whole chain from the
252
- // invite alone). A root or legacy identity emits the original shape
253
- // byte-for-byte, so those invites stay redeemable by old clients.
254
- if delegation_cert != NIL && root_profile != NIL
255
- {
256
- role_invite is a2a_protocol::invite_role_t = (
257
- $d -> invite_id,
258
- $n -> my_name,
259
- $c -> my_identity $container_id,
260
- $k -> my_identity $key_list,
261
- $a -> my_ad $authorizations,
262
- $b -> my_bio,
263
- $dc -> delegation_cert?,
264
- $rp -> root_profile?
265
- ).
266
- return transaction::success [
267
- _return_data (
268
- $invite -> (_write role_invite),
269
- $invite_id -> invite_id,
270
- $peer_name -> assigned
271
- ),
272
- _save_state NIL
273
- ].
274
- }
275
-
276
- invite is a2a_protocol::invite_t = (
277
- $d -> invite_id,
278
- $n -> my_name,
279
- $c -> my_identity $container_id,
280
- $k -> my_identity $key_list,
281
- $a -> my_ad $authorizations
280
+ // Wire the agent's storage into the shared messaging core. The receive
281
+ // hook owns everything app-specific about an inbound message: known
282
+ // senders deposit into the inbox; an unknown sender may be a verified-
283
+ // but-unapproved local introduction messaging before approval — queue
284
+ // (bounded) inside its pending entry, approval flushes the queue into
285
+ // the inbox in order; anything else from an unknown sender is rejected.
286
+ // The notification deliberately carries NO message body — only that a
287
+ // message arrived, from whom, and its id. The body stays in the packet
288
+ // and only leaves through get_messages.
289
+ a2a_messaging::init (
290
+ $_read_or_abort -> _read_or_abort,
291
+ $on_message_received -> fn (arg: any) -> transaction::action::type[]
292
+ {
293
+ sender_id = (arg $sender_id) safe global_id.
294
+ text = (arg $text) safe str.
295
+ msg_date = (arg $date) safe time.
296
+
297
+ if (arg $sender_name) == NIL
298
+ {
299
+ p = pending_introductions sender_id.
300
+ abort "Message from an unknown sender was rejected." when p == NIL.
301
+ entry = p?.
302
+ queued = entry $messages.
303
+ abort "Pending-introduction message queue is full; awaiting approval." when (_count queued|) >= pending_queue_cap.
304
+ queued (_count queued|) -> ($text -> text, $date -> msg_date).
305
+ pending_introductions sender_id -> ($name -> entry $name, $ad -> entry $ad, $messages -> queued).
306
+ return [
307
+ _notify_agent ($event -> $pending_message, $sender_name -> entry $name, $queued -> _count queued|),
308
+ _save_state NIL
309
+ ].
310
+ }
311
+
312
+ sender_name = (arg $sender_name) safe str.
313
+ mid = deposit_message sender_id sender_name text msg_date.
314
+ actions is transaction::action::type[] = [].
315
+ sc monitor_copy_actions "in" sender_id text msg_date -- ( -> a)
316
+ {
317
+ actions (_count actions|) -> a.
318
+ }
319
+ actions (_count actions|) -> _notify_agent ($event -> $message_received, $sender_name -> sender_name, $msg_id -> mid, $date -> msg_date).
320
+ actions (_count actions|) -> _save_state NIL.
321
+ return actions.
322
+ },
323
+ $on_message_sent -> fn (arg: any) -> transaction::action::type[]
324
+ {
325
+ return monitor_copy_actions "out" ((arg $target_id) safe global_id) ((arg $text) safe str) ((arg $date) safe time).
326
+ },
327
+ $on_contact_removed -> fn (_: any) -> transaction::action::type[] { return []. }
282
328
  ).
283
329
 
284
- return transaction::success [
285
- _return_data (
286
- $invite -> (_write invite),
287
- $invite_id -> invite_id,
288
- $peer_name -> assigned
289
- ),
290
- _save_state NIL
291
- ].
292
- }
293
-
294
- trn add_contact _:($invite -> invite_blob: bin, $name -> custom_name: str+)
295
- {
296
- current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
297
-
298
- // Parse field-by-field rather than via one `safe invite_t` cast: the
299
- // shared fields are identical between invite_t and invite_role_t, so
300
- // this one path redeems both the legacy shape and the role shape (the
301
- // hierarchy fields are simply NIL on a legacy invite).
302
- raw = _read_or_abort invite_blob.
303
- inviter_id = (raw $c) safe global_id.
304
- abort "This invite is your own — you cannot add yourself." when inviter_id == _get_container_id().
305
- invite_id = (raw $d) safe global_id.
306
- inviter_name = (raw $n) safe str.
307
- inviter_keys = (raw $k) safe (key_utils::t_publickey(,)).
308
- inviter_auths = (raw $a) safe (crypto_signature(,)).
309
-
310
- // Rebuild the inviter's full address document from the carried material
311
- // (see a2a_protocol::rebuild_peer_address_document — the reconstructed
312
- // identity is byte-for-byte the signed one). import_state later replays
313
- // this document through process_address_document to re-register the
314
- // inviter's keys after a code upgrade — so it must validate, and it does.
315
- inviter_ad = a2a_protocol::rebuild_peer_address_document inviter_id inviter_keys inviter_auths.
316
-
317
- // A role invite carries a delegation chain — verify it BEFORE anything
318
- // is registered (an invalid chain rejects the whole invite), and record
319
- // the root linkage. A legacy/root invite has no chain; nothing to check.
320
- inviter_root is a2a_protocol::contact_root_t+ = NIL.
321
- inviter_bio is str = "".
322
- if (raw $dc) != NIL
323
- {
324
- cert = (raw $dc) safe a2a_protocol::delegation_cert_t.
325
- rp = (raw $rp) safe a2a_protocol::root_profile_t.
326
- inviter_root -> a2a_protocol::verify_peer_delegation inviter_id (_value_id inviter_ad) cert rp.
327
- inviter_bio -> (raw $b) safe str.
328
- }
329
-
330
- // Register the inviter under my chosen name, or the name they embedded.
331
- contact_name = (custom_name == NIL ?? inviter_name ; custom_name?).
332
- contacts inviter_id -> ($name -> contact_name, $container_id -> inviter_id).
333
- // Remember the inviter's address document so I can re-register them after
334
- // an upgrade (their keys are seed-stable, so this stays valid).
335
- peer_ads inviter_id -> inviter_ad.
336
- if inviter_root != NIL
337
- {
338
- contact_roots inviter_id -> inviter_root?.
339
- }
340
-
341
- my_self_name = my_name.
342
- my_ad = address_document::get_my_address_document().
343
- // If I am a delegated role myself, carry my own chain in the reply so
344
- // the inviter learns my root linkage symmetrically.
345
- my_cert_blob is bin+ = NIL.
346
- my_rp_blob is bin+ = NIL.
347
- if delegation_cert != NIL && root_profile != NIL
348
- {
349
- my_cert_blob -> (_write delegation_cert?).
350
- my_rp_blob -> (_write root_profile?).
351
- }
352
-
353
- root_name_out is str = "".
354
- role_id_out is str = "".
355
- if inviter_root != NIL
356
- {
357
- root_name_out -> inviter_root? $root_name.
358
- role_id_out -> inviter_root? $role_id.
359
- }
360
-
361
- // Establish the encrypted channel with the inviter (handshake happens
362
- // transparently if we haven't talked before), then tell them I redeemed
363
- // their invite, what my own name is, and my address document, so they can
364
- // finish the handshake and remember me for future upgrades.
365
- return encrypted_channel::execute_transaction inviter_id (fn (_) -> transaction::results::type {
366
- return transaction::success [
367
- encrypted_channel::send_encrypted_tx inviter_id (
368
- $name -> "::actor::accept_contact",
369
- $targ -> (
370
- $invite_id -> invite_id,
371
- $joiner_name -> my_self_name,
372
- $joiner_ad -> my_ad,
373
- $joiner_cert -> my_cert_blob,
374
- $joiner_root_profile -> my_rp_blob
375
- )
376
- ),
377
- _return_data (
378
- $added -> contact_name,
379
- $container_id -> inviter_id,
380
- $root_name -> root_name_out,
381
- $role_id -> role_id_out,
382
- $bio -> inviter_bio
383
- ),
384
- _save_state NIL
385
- ].
386
- }).
387
- }
388
-
389
- trn send_message _:($contact -> contact_ref: str, $text -> text: str)
390
- {
391
- current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
392
-
393
- target_id = resolve_contact contact_ref.
394
-
395
- return encrypted_channel::execute_transaction target_id (fn (_) -> transaction::results::type {
396
- return transaction::success [
397
- encrypted_channel::send_encrypted_tx target_id (
398
- $name -> "::actor::receive_message",
399
- $targ -> ($text -> text)
400
- ),
401
- _return_data ($sent_to -> target_id, $text -> text),
402
- _save_state NIL
403
- ].
404
- }).
405
- }
406
-
407
- trn remove_contact _:($contact -> contact_ref: str)
408
- {
409
- current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
410
-
411
- target_id = resolve_contact contact_ref.
412
- removed = contacts target_id.
413
- removed_name = removed? $name.
414
-
415
- delete contacts target_id.
416
- if peer_ads target_id != NIL { delete peer_ads target_id. }
417
- if contact_roots target_id != NIL { delete contact_roots target_id. }
418
-
419
- return transaction::success [
420
- _return_data ($removed -> removed_name, $container_id -> target_id),
421
- _save_state NIL
422
- ].
330
+ // Wire the control plane (see MONITORING-AND-SHARED-LIBRARY-DESIGN.md
331
+ // Part 4): control requests from the bound browser proxy queue in
332
+ // control_inbox — NEVER the message inbox, so agent sessions don't see
333
+ // them — and the notify event wakes the daemon's dispatcher. The
334
+ // payload stays opaque here; sender authorization happens in the
335
+ // daemon against the packet's monitoring_proxy / proxy_pending state.
336
+ a2a_control::init (
337
+ $on_control_received -> fn (arg: any) -> transaction::action::type[]
338
+ {
339
+ abort "Control queue is full." when (_count control_inbox|) >= control_inbox_cap.
340
+ sender_name = (arg $sender_name) safe str.
341
+ control_inbox (_count control_inbox|) -> (
342
+ $sender_cid -> (arg $sender_id) safe global_id,
343
+ $sender_name -> sender_name,
344
+ $payload -> (arg $payload) safe str,
345
+ $date -> (arg $date) safe time
346
+ ).
347
+ return [
348
+ _notify_agent ($event -> $control_request, $sender_name -> sender_name, $queued -> _count control_inbox|),
349
+ _save_state NIL
350
+ ].
351
+ }
352
+ ).
423
353
  }
424
354
 
425
- trn readonly list_contacts _
426
- {
427
- return contacts.
428
- }
355
+ // ---- message store --------------------------------------------------------
429
356
 
430
357
  trn readonly list_incoming_messages _
431
358
  {
@@ -668,10 +595,10 @@ application actor loads libraries
668
595
  entry_sig = (_read_or_abort entry_sig_blob) safe crypto_signature.
669
596
  abort "Contact-book entry failed registrar verification." when key_storage::check_signature_new_container (_value_id entry) entry_sig (registrar_ad? $identity $key_list) != TRUE.
670
597
 
671
- contacts target_id -> ($name -> name, $container_id -> target_id).
672
- peer_ads target_id -> target_ad.
598
+ a2a_messaging::contacts target_id -> ($name -> name, $container_id -> target_id).
599
+ a2a_messaging::peer_ads target_id -> target_ad.
673
600
 
674
- my_self_name = my_name.
601
+ my_self_name = a2a_messaging::my_name.
675
602
  my_ad = address_document::get_my_address_document().
676
603
  return encrypted_channel::execute_transaction target_id (fn (_) -> transaction::results::type {
677
604
  return transaction::success [
@@ -691,8 +618,8 @@ application actor loads libraries
691
618
 
692
619
  pid = resolve_pending ref.
693
620
  entry = (pending_introductions pid)?.
694
- contacts pid -> ($name -> entry $name, $container_id -> pid).
695
- peer_ads pid -> entry $ad.
621
+ a2a_messaging::contacts pid -> ($name -> entry $name, $container_id -> pid).
622
+ a2a_messaging::peer_ads pid -> entry $ad.
696
623
 
697
624
  queued = entry $messages.
698
625
  flushed is int = 0.
@@ -751,7 +678,7 @@ application actor loads libraries
751
678
  trn sign_delegation _:($role_ad -> role_ad_blob: bin, $role_id -> role_id: str)
752
679
  {
753
680
  current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
754
- abort "Only a root identity can sign delegation certificates." when delegation_cert != NIL.
681
+ abort "Only a root identity can sign delegation certificates." when a2a_messaging::delegation_cert != NIL.
755
682
 
756
683
  role_ad = (_read_or_abort role_ad_blob) safe address_document_types::t_address_document.
757
684
  role_cid = role_ad $identity $container_id.
@@ -777,14 +704,14 @@ application actor loads libraries
777
704
  trn export_root_profile _
778
705
  {
779
706
  current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
780
- abort "Only a root identity can export a root profile." when delegation_cert != NIL.
707
+ abort "Only a root identity can export a root profile." when a2a_messaging::delegation_cert != NIL.
781
708
 
782
709
  my_ad = address_document::get_my_address_document().
783
710
  core is a2a_protocol::root_profile_core_t = (
784
711
  $version -> 1,
785
712
  $root_cid -> _get_container_id(),
786
- $name -> my_name,
787
- $bio -> my_bio,
713
+ $name -> a2a_messaging::my_name,
714
+ $bio -> a2a_messaging::my_bio,
788
715
  $keys -> my_ad $identity $key_list
789
716
  ).
790
717
  profile is a2a_protocol::root_profile_t = ($p -> core, $s -> key_storage::default_sign (_value_id core)).
@@ -817,9 +744,9 @@ application actor loads libraries
817
744
  abort "The root profile's key list does not match the root's address document." when (_value_id (rp $p $keys)) != (_value_id (new_root_ad $identity $key_list)).
818
745
  abort "The root profile signature is invalid." when key_storage::check_signature_new_container (_value_id (rp $p)) (rp $s) (new_root_ad $identity $key_list) != TRUE.
819
746
 
820
- delegation_cert -> cert.
821
- root_ad -> new_root_ad.
822
- root_profile -> rp.
747
+ a2a_messaging::delegation_cert -> cert.
748
+ a2a_messaging::root_ad -> new_root_ad.
749
+ a2a_messaging::root_profile -> rp.
823
750
 
824
751
  return transaction::success [
825
752
  _return_data ($delegated -> TRUE, $root_cid -> (_str (cert $c $root_cid)), $role_id -> cert $c $role_id),
@@ -829,34 +756,24 @@ application actor loads libraries
829
756
 
830
757
  trn readonly describe_identity _
831
758
  {
832
- if delegation_cert == NIL
759
+ if a2a_messaging::delegation_cert == NIL
833
760
  {
834
- return ($name -> my_name, $bio -> my_bio, $has_cert -> FALSE, $role_id -> "", $root_cid -> "", $root_name -> "").
761
+ return ($name -> a2a_messaging::my_name, $bio -> a2a_messaging::my_bio, $has_cert -> FALSE, $role_id -> "", $root_cid -> "", $root_name -> "", $monitoring_enabled -> monitoring_enabled).
835
762
  }
836
- cert = delegation_cert?.
763
+ cert = a2a_messaging::delegation_cert?.
837
764
  rname is str = "".
838
- if root_profile != NIL { rname -> root_profile? $p $name. }
765
+ if a2a_messaging::root_profile != NIL { rname -> a2a_messaging::root_profile? $p $name. }
839
766
  return (
840
- $name -> my_name,
841
- $bio -> my_bio,
767
+ $name -> a2a_messaging::my_name,
768
+ $bio -> a2a_messaging::my_bio,
842
769
  $has_cert -> TRUE,
843
770
  $role_id -> cert $c $role_id,
844
771
  $root_cid -> (_str (cert $c $root_cid)),
845
- $root_name -> rname
772
+ $root_name -> rname,
773
+ $monitoring_enabled -> monitoring_enabled
846
774
  ).
847
775
  }
848
776
 
849
- trn readonly list_contact_roots _
850
- {
851
- return contact_roots.
852
- }
853
-
854
- // The shared-core version this packet was compiled with (see core/version.mm).
855
- trn readonly get_version _
856
- {
857
- return ($core -> version::get_core_version()).
858
- }
859
-
860
777
  // Connect to an intra-root sibling (Ring 1): register it as a contact and
861
778
  // introduce myself over the encrypted channel, presenting my delegation
862
779
  // cert (NIL when I am the root itself — the channel proves I control the
@@ -872,22 +789,22 @@ application actor loads libraries
872
789
  abort "This sibling is your own identity." when target_id == _get_container_id().
873
790
 
874
791
  cert_blob is bin+ = NIL.
875
- if delegation_cert != NIL { cert_blob -> (_write delegation_cert?). }
792
+ if a2a_messaging::delegation_cert != NIL { cert_blob -> (_write a2a_messaging::delegation_cert?). }
876
793
 
877
- contacts target_id -> ($name -> name, $container_id -> target_id).
878
- peer_ads target_id -> target_ad.
794
+ a2a_messaging::contacts target_id -> ($name -> name, $container_id -> target_id).
795
+ a2a_messaging::peer_ads target_id -> target_ad.
879
796
  // Record the target's root linkage: a sibling shares my root by
880
797
  // definition (the receiving side verifies the converse independently).
881
- if delegation_cert != NIL && root_profile != NIL
798
+ if a2a_messaging::delegation_cert != NIL && a2a_messaging::root_profile != NIL
882
799
  {
883
- contact_roots target_id -> ($root_cid -> delegation_cert? $c $root_cid, $root_name -> root_profile? $p $name, $role_id -> name).
800
+ a2a_messaging::contact_roots target_id -> ($root_cid -> a2a_messaging::delegation_cert? $c $root_cid, $root_name -> a2a_messaging::root_profile? $p $name, $role_id -> name).
884
801
  }
885
802
  else
886
803
  {
887
- contact_roots target_id -> ($root_cid -> _get_container_id(), $root_name -> my_name, $role_id -> name).
804
+ a2a_messaging::contact_roots target_id -> ($root_cid -> _get_container_id(), $root_name -> a2a_messaging::my_name, $role_id -> name).
888
805
  }
889
806
 
890
- my_self_name = my_name.
807
+ my_self_name = a2a_messaging::my_name.
891
808
  my_ad = address_document::get_my_address_document().
892
809
  return encrypted_channel::execute_transaction target_id (fn (_) -> transaction::results::type {
893
810
  return transaction::success [
@@ -901,6 +818,232 @@ application actor loads libraries
901
818
  }).
902
819
  }
903
820
 
821
+ // ---- monitoring + control plane -------------------------------------------
822
+ // (see MONITORING-AND-SHARED-LIBRARY-DESIGN.md). A monitored ROLE reports
823
+ // every message it sends/receives to its ROOT (the monitor_copy_actions
824
+ // branches in the storage hooks above); the root queues the copies and the
825
+ // host forwards them to a human proxy bound via 6-digit-code verification.
826
+ // The proxy's control requests (create agent, update role, …) queue in
827
+ // control_inbox and are executed by the host daemon.
828
+
829
+ trn readonly get_monitoring_status _
830
+ {
831
+ pending is bool = FALSE.
832
+ if proxy_pending != NIL { pending -> TRUE. }
833
+ proxy_out is str = "".
834
+ if monitoring_proxy != NIL { proxy_out -> _str (monitoring_proxy? $proxy_cid). }
835
+ return (
836
+ $monitoring_enabled -> monitoring_enabled,
837
+ $proxy_cid -> proxy_out,
838
+ $proxy_pending -> pending,
839
+ $copies_queued -> _count monitoring_inbox|,
840
+ $control_queued -> _count control_inbox|
841
+ ).
842
+ }
843
+
844
+ // Sign a monitoring authorization for a role (ROOT packet only — the role
845
+ // verifies the signature against its pinned root keys, so an auth minted
846
+ // by any other packet fails). Stateless, mirrors sign_delegation.
847
+ trn sign_monitoring_auth _:($role_ad -> role_ad_blob: bin, $enabled -> enabled: bool)
848
+ {
849
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
850
+ abort "Only a root identity can sign monitoring authorizations." when a2a_messaging::delegation_cert != NIL.
851
+
852
+ role_ad = (_read_or_abort role_ad_blob) safe address_document_types::t_address_document.
853
+ core is monitoring_auth_core_t = (
854
+ $version -> 1,
855
+ $role_cid -> role_ad $identity $container_id,
856
+ $enabled -> enabled,
857
+ $issued_at -> (current_transaction_info::get_transaction_time())?
858
+ ).
859
+ auth is monitoring_auth_t = ($c -> core, $s -> key_storage::default_sign (_value_id core)).
860
+ return transaction::success [
861
+ _return_data ($auth -> (_write auth))
862
+ ].
863
+ }
864
+
865
+ // Store a verified monitoring flag (ROLE packet, host-fired after the root
866
+ // signed the auth). An auth that does not name me or was not signed by my
867
+ // root is rejected — so even a compromised host process cannot silently
868
+ // flip monitoring without the root packet's keys.
869
+ trn set_monitoring _:($auth -> auth_blob: bin)
870
+ {
871
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
872
+ abort "Only a delegated role can be monitored." when a2a_messaging::delegation_cert == NIL || a2a_messaging::root_ad == NIL.
873
+
874
+ auth = (_read_or_abort auth_blob) safe monitoring_auth_t.
875
+ abort "Unsupported monitoring authorization version." when (auth $c $version) != 1.
876
+ abort "This monitoring authorization was issued to a different identity." when (auth $c $role_cid) != _get_container_id().
877
+ abort "The monitoring authorization was not signed by my root." when key_storage::check_signature_new_container (_value_id (auth $c)) (auth $s) (a2a_messaging::root_ad? $identity $key_list) != TRUE.
878
+
879
+ monitoring_enabled -> auth $c $enabled.
880
+ return transaction::success [
881
+ _return_data ($monitoring_enabled -> monitoring_enabled),
882
+ _save_state NIL
883
+ ].
884
+ }
885
+
886
+ // A monitored role reports one message copy (ROOT packet, inbound). Only
887
+ // accepted from a verified role of THIS root (the contact_roots linkage
888
+ // recorded by sibling_introduce), and the copy must name its actual sender
889
+ // — a role cannot forge copies on another role's behalf.
890
+ trn receive_monitoring_copy _:($copy -> copy: monitoring_copy_t)
891
+ {
892
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::external,).
893
+ encrypted_channel::check_encrypted_or_abort().
894
+
895
+ sender_id = current_transaction_info::get_external_envelope_or_abort() $from.
896
+ link = a2a_messaging::contact_roots sender_id.
897
+ abort "Monitoring copies are only accepted from my own roles." when link == NIL || (link? $root_cid) != _get_container_id().
898
+ abort "Monitoring copy does not name its sender as the source." when (copy $source_cid) != sender_id.
899
+ abort "Unsupported monitoring copy version." when (copy $version) != 1.
900
+
901
+ // Capped queue, oldest first out: if no proxy drains the root, recent
902
+ // traffic wins over old.
903
+ if (_count monitoring_inbox|) >= monitoring_inbox_cap
904
+ {
905
+ trimmed is monitoring_copy_t[] = [].
906
+ i is int = 0.
907
+ sc monitoring_inbox -- ( -> m)
908
+ {
909
+ if i > 0 { trimmed (_count trimmed|) -> m. }
910
+ i -> i + 1.
911
+ }
912
+ monitoring_inbox -> trimmed.
913
+ }
914
+ monitoring_inbox (_count monitoring_inbox|) -> copy.
915
+
916
+ return transaction::success [
917
+ _notify_agent ($event -> $monitoring_copy, $source_name -> copy $source_name, $queued -> _count monitoring_inbox|),
918
+ _save_state NIL
919
+ ].
920
+ }
921
+
922
+ // Drain the queued monitoring copies (ROOT packet, host-fired before
923
+ // forwarding to the bound proxy). Cleared on read, like get_messages.
924
+ trn get_monitoring_copies _
925
+ {
926
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
927
+
928
+ copies = monitoring_inbox.
929
+ monitoring_inbox -> [].
930
+ return transaction::success [
931
+ _return_data ($copies -> copies),
932
+ _save_state NIL
933
+ ].
934
+ }
935
+
936
+ // Drain the queued control requests (ROOT packet, host-fired by the
937
+ // control dispatcher). Cleared on read.
938
+ trn get_control_requests _
939
+ {
940
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
941
+
942
+ reqs = control_inbox.
943
+ control_inbox -> [].
944
+ return transaction::success [
945
+ _return_data ($requests -> reqs),
946
+ _save_state NIL
947
+ ].
948
+ }
949
+
950
+ // Start a proxy binding (ROOT packet, host-fired): remember the code the
951
+ // host generated (MUFL has no random source) for one specific contact.
952
+ // Restarting overwrites any previous pending binding.
953
+ trn set_proxy_pending _:($code -> code: str, $proxy -> proxy_ref: str)
954
+ {
955
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
956
+ abort "Only a root identity can bind a monitoring proxy." when a2a_messaging::delegation_cert != NIL.
957
+
958
+ pid = a2a_messaging::resolve_contact proxy_ref.
959
+ proxy_pending -> (
960
+ $code -> code,
961
+ $proxy_cid -> pid,
962
+ $created_at -> (current_transaction_info::get_transaction_time())?,
963
+ $attempts -> 0
964
+ ).
965
+ return transaction::success [
966
+ _return_data ($pending -> TRUE, $proxy_cid -> (_str pid)),
967
+ _save_state NIL
968
+ ].
969
+ }
970
+
971
+ // Verify a proxy's code attempt (ROOT packet, host-fired when a `bind`
972
+ // control request arrives). Failures are returned as DATA — not aborts —
973
+ // so the attempt counter and expiry clearing persist atomically; an abort
974
+ // would roll them back and reopen the brute-force window.
975
+ trn verify_proxy_code _:($code -> code: str, $sender -> sender_ref: str)
976
+ {
977
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
978
+
979
+ if proxy_pending == NIL
980
+ {
981
+ return transaction::success [ _return_data ($verified -> FALSE, $reason -> "no_pending") ].
982
+ }
983
+ p = proxy_pending?.
984
+ now = (current_transaction_info::get_transaction_time())?.
985
+
986
+ if (_substract_seconds now (p $created_at)) > proxy_code_max_age_seconds
987
+ {
988
+ proxy_pending -> NIL.
989
+ return transaction::success [
990
+ _return_data ($verified -> FALSE, $reason -> "expired"),
991
+ _save_state NIL
992
+ ].
993
+ }
994
+
995
+ sid = a2a_messaging::resolve_contact sender_ref.
996
+ if sid != (p $proxy_cid)
997
+ {
998
+ // Not the contact this binding was started for: reject without
999
+ // burning an attempt (the code was never compared).
1000
+ return transaction::success [ _return_data ($verified -> FALSE, $reason -> "wrong_sender") ].
1001
+ }
1002
+
1003
+ if code != (p $code)
1004
+ {
1005
+ attempts = (p $attempts) + 1.
1006
+ if attempts >= proxy_max_attempts
1007
+ {
1008
+ proxy_pending -> NIL.
1009
+ return transaction::success [
1010
+ _return_data ($verified -> FALSE, $reason -> "too_many_attempts"),
1011
+ _save_state NIL
1012
+ ].
1013
+ }
1014
+ proxy_pending -> (
1015
+ $code -> p $code,
1016
+ $proxy_cid -> p $proxy_cid,
1017
+ $created_at -> p $created_at,
1018
+ $attempts -> attempts
1019
+ ).
1020
+ return transaction::success [
1021
+ _return_data ($verified -> FALSE, $reason -> "wrong_code", $attempts_left -> proxy_max_attempts - attempts),
1022
+ _save_state NIL
1023
+ ].
1024
+ }
1025
+
1026
+ monitoring_proxy -> ($proxy_cid -> sid, $bound_at -> now).
1027
+ proxy_pending -> NIL.
1028
+ return transaction::success [
1029
+ _return_data ($verified -> TRUE, $proxy_cid -> (_str sid)),
1030
+ _save_state NIL
1031
+ ].
1032
+ }
1033
+
1034
+ // Drop the proxy binding (and any in-flight code verification).
1035
+ trn clear_monitoring_proxy _
1036
+ {
1037
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
1038
+
1039
+ monitoring_proxy -> NIL.
1040
+ proxy_pending -> NIL.
1041
+ return transaction::success [
1042
+ _return_data ($cleared -> TRUE),
1043
+ _save_state NIL
1044
+ ].
1045
+ }
1046
+
904
1047
  // ---- upgrade: state export / import -------------------------------------
905
1048
  // The host persists state by calling export_state (readonly) and serializing
906
1049
  // the returned value to a code-independent blob. On a code upgrade it recreates
@@ -909,27 +1052,37 @@ application actor loads libraries
909
1052
  //
910
1053
  // The packet-level snapshot is NOT used for upgrades: it is bound to the unit
911
1054
  // code hash, so a new .muflo cannot load an old snapshot. This data blob is.
1055
+ //
1056
+ // The blob stays FLAT with the historical field names — the core fields come
1057
+ // from a2a_messaging::export_core_state / import_core_state, the app fields
1058
+ // are composed in here — so a PRE-migration export imports unchanged.
912
1059
 
913
1060
  trn readonly export_state _
914
1061
  {
1062
+ core_state = a2a_messaging::export_core_state NIL.
915
1063
  return (
916
- $my_name -> my_name,
917
- $contacts -> contacts,
918
- $pending_invites -> pending_invites,
1064
+ $my_name -> core_state $my_name,
1065
+ $contacts -> core_state $contacts,
1066
+ $pending_invites -> core_state $pending_invites,
919
1067
  $inbox -> inbox,
920
1068
  $next_msg_seq -> next_msg_seq,
921
- $peer_ads -> peer_ads,
1069
+ $peer_ads -> core_state $peer_ads,
922
1070
  $registrar_ad -> registrar_ad,
923
1071
  $local_auto_accept -> local_auto_accept,
924
1072
  // Nonces are exported so a restart does not reopen the replay window
925
1073
  // for still-fresh credentials; stale ones are purged lazily anyway.
926
1074
  $seen_nonces -> seen_nonces,
927
1075
  $pending_introductions -> pending_introductions,
928
- $my_bio -> my_bio,
929
- $delegation_cert -> delegation_cert,
930
- $root_ad -> root_ad,
931
- $root_profile -> root_profile,
932
- $contact_roots -> contact_roots
1076
+ $my_bio -> core_state $my_bio,
1077
+ $delegation_cert -> core_state $delegation_cert,
1078
+ $root_ad -> core_state $root_ad,
1079
+ $root_profile -> core_state $root_profile,
1080
+ $contact_roots -> core_state $contact_roots,
1081
+ $monitoring_enabled -> monitoring_enabled,
1082
+ $monitoring_inbox -> monitoring_inbox,
1083
+ $proxy_pending -> proxy_pending,
1084
+ $monitoring_proxy -> monitoring_proxy,
1085
+ $control_inbox -> control_inbox
933
1086
  ).
934
1087
  }
935
1088
 
@@ -937,12 +1090,11 @@ application actor loads libraries
937
1090
  {
938
1091
  current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
939
1092
 
940
- // The fields that did not change across the schema bump are validated the
941
- // same way for any version of the blob.
942
- my_name -> (data $my_name) safe str.
943
- contacts -> (data $contacts) safe (global_id ->> a2a_protocol::contact_t).
944
- pending_invites -> (data $pending_invites) safe (global_id ->> str).
945
- peer_ads -> (data $peer_ads) safe (global_id ->> address_document_types::t_address_document).
1093
+ // Core fields (contacts/profile/hierarchy) validated and restored by
1094
+ // the shared library, which also replays every peer's address document
1095
+ // through process_address_document so encrypted channels keep working
1096
+ // after the upgrade with no re-handshake.
1097
+ a2a_messaging::import_core_state data.
946
1098
 
947
1099
  // The inbox + next_msg_seq are the only parts the message-lifecycle changes
948
1100
  // touched. A pre-lifecycle blob has no $next_msg_seq and inbox entries with
@@ -1013,36 +1165,29 @@ application actor loads libraries
1013
1165
  pending_introductions -> (data $pending_introductions) safe (global_id ->> pending_intro_t).
1014
1166
  }
1015
1167
 
1016
- // Identity-hierarchy state arrived after the local-contact-book schema —
1017
- // same pattern: every field is optional, defaults stay when absent.
1018
- if (data $my_bio) != NIL
1168
+ // Monitoring + control state arrived after the local-book schema —
1169
+ // optional in old blobs the same way.
1170
+ if (data $monitoring_enabled) != NIL
1019
1171
  {
1020
- my_bio -> (data $my_bio) safe str.
1172
+ monitoring_enabled -> (data $monitoring_enabled) safe bool.
1021
1173
  }
1022
- if (data $delegation_cert) != NIL
1174
+ if (data $monitoring_inbox) != NIL
1023
1175
  {
1024
- delegation_cert -> (data $delegation_cert) safe a2a_protocol::delegation_cert_t.
1176
+ monitoring_inbox -> (data $monitoring_inbox) safe (monitoring_copy_t[]).
1025
1177
  }
1026
- if (data $root_ad) != NIL
1178
+ if (data $proxy_pending) != NIL
1027
1179
  {
1028
- root_ad -> (data $root_ad) safe address_document_types::t_address_document.
1180
+ proxy_pending -> (data $proxy_pending) safe proxy_pending_t.
1029
1181
  }
1030
- if (data $root_profile) != NIL
1182
+ if (data $monitoring_proxy) != NIL
1031
1183
  {
1032
- root_profile -> (data $root_profile) safe a2a_protocol::root_profile_t.
1184
+ monitoring_proxy -> (data $monitoring_proxy) safe proxy_binding_t.
1033
1185
  }
1034
- if (data $contact_roots) != NIL
1186
+ if (data $control_inbox) != NIL
1035
1187
  {
1036
- contact_roots -> (data $contact_roots) safe (global_id ->> a2a_protocol::contact_root_t).
1188
+ control_inbox -> (data $control_inbox) safe (control_req_t[]).
1037
1189
  }
1038
1190
 
1039
- // Re-register every peer's keys so encrypted channels keep working after
1040
- // the upgrade — no handshake needed (my own keys are unchanged, and the
1041
- // peers' self-signed address documents re-authorize on this fresh packet).
1042
- sc peer_ads -- ( -> ad)
1043
- {
1044
- address_document::process_address_document ad TRUE.
1045
- }
1046
1191
  // Pending introducers' keys too: their channel to me predates approval,
1047
1192
  // so it must survive an upgrade exactly like an approved contact's.
1048
1193
  sc pending_introductions -- ( -> p)
@@ -1051,103 +1196,24 @@ application actor loads libraries
1051
1196
  }
1052
1197
 
1053
1198
  return transaction::success [
1054
- _return_data ($imported -> TRUE, $contacts -> _count contacts|, $peers -> _count peer_ads|),
1199
+ _return_data ($imported -> TRUE, $contacts -> _count a2a_messaging::contacts|, $peers -> _count a2a_messaging::peer_ads|),
1055
1200
  _save_state NIL
1056
1201
  ].
1057
1202
  }
1058
1203
 
1059
1204
  // ---- external (inbound) transactions ------------------------------------
1060
1205
 
1061
- // Args are taken as `any` (not a destructured shape) so old clients — whose
1062
- // accept_contact payload has no hierarchy fields keep working unchanged.
1206
+ // Compat shims (Option A): pre-migration peers address these as ::actor::*,
1207
+ // and the core keeps SENDING to the ::actor:: names too. Each delegates to
1208
+ // the shared handler — remove only when no old clients remain.
1063
1209
  trn accept_contact args: any
1064
1210
  {
1065
- current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::external,).
1066
- encrypted_channel::check_encrypted_or_abort().
1067
-
1068
- sender_id = current_transaction_info::get_external_envelope_or_abort() $from.
1069
- invite_id = (args $invite_id) safe global_id.
1070
- joiner_ad = (args $joiner_ad) safe address_document_types::t_address_document.
1071
-
1072
- // Only an invite I generated (and have not yet consumed) authorizes a
1073
- // contact registration. Without this gate any invite blob would be a
1074
- // multi-use bearer credential: anyone who ever saw one could register
1075
- // themselves as my contact with a self-chosen name.
1076
- assigned_name = pending_invites invite_id.
1077
- abort "Unknown or already-redeemed invite." when assigned_name == NIL.
1078
- // An empty assigned name means the invite was generated without one:
1079
- // the joiner's self-announced name wins (container id as a last resort
1080
- // when the joiner never set a name either).
1081
- contact_name is str = assigned_name?.
1082
- if contact_name == ""
1083
- {
1084
- joiner_self = (args $joiner_name) safe str.
1085
- contact_name -> (joiner_self == "" ?? (_str sender_id) ; joiner_self).
1086
- }
1087
-
1088
- // A delegated-role joiner carries its chain so I learn its root linkage
1089
- // symmetrically; an invalid chain rejects the redemption outright.
1090
- joiner_root is a2a_protocol::contact_root_t+ = NIL.
1091
- if (args $joiner_cert) != NIL
1092
- {
1093
- cert = (_read_or_abort ((args $joiner_cert) safe bin)) safe a2a_protocol::delegation_cert_t.
1094
- rp = (_read_or_abort ((args $joiner_root_profile) safe bin)) safe a2a_protocol::root_profile_t.
1095
- joiner_root -> a2a_protocol::verify_peer_delegation sender_id (_value_id joiner_ad) cert rp.
1096
- }
1097
-
1098
- contacts sender_id -> ($name -> contact_name, $container_id -> sender_id).
1099
- // Remember the joiner's address document for upgrade-time re-registration.
1100
- peer_ads sender_id -> joiner_ad.
1101
- if joiner_root != NIL
1102
- {
1103
- contact_roots sender_id -> joiner_root?.
1104
- }
1105
- delete pending_invites invite_id.
1106
-
1107
- return transaction::success [
1108
- _notify_agent ($event -> $contact_accepted, $name -> contact_name, $container_id -> sender_id),
1109
- _save_state NIL
1110
- ].
1211
+ return a2a_messaging::handle_accept_contact args.
1111
1212
  }
1112
1213
 
1113
1214
  trn receive_message _:($text -> text: str)
1114
1215
  {
1115
- current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::external,).
1116
- encrypted_channel::check_encrypted_or_abort().
1117
-
1118
- sender_id = current_transaction_info::get_external_envelope_or_abort() $from.
1119
- msg_date = (current_transaction_info::get_transaction_time())?.
1120
- sender = contacts sender_id.
1121
-
1122
- if sender == NIL
1123
- {
1124
- // A verified-but-unapproved local introduction may message me before
1125
- // approval: queue (bounded) inside its pending entry. Approval
1126
- // flushes the queue into the inbox in order; anything else from an
1127
- // unknown sender is rejected as before.
1128
- p = pending_introductions sender_id.
1129
- abort "Message from an unknown sender was rejected." when p == NIL.
1130
- entry = p?.
1131
- queued = entry $messages.
1132
- abort "Pending-introduction message queue is full; awaiting approval." when (_count queued|) >= pending_queue_cap.
1133
- queued (_count queued|) -> ($text -> text, $date -> msg_date).
1134
- pending_introductions sender_id -> ($name -> entry $name, $ad -> entry $ad, $messages -> queued).
1135
- return transaction::success [
1136
- _notify_agent ($event -> $pending_message, $sender_name -> entry $name, $queued -> _count queued|),
1137
- _save_state NIL
1138
- ].
1139
- }
1140
-
1141
- sender_name = sender? $name.
1142
- mid = deposit_message sender_id sender_name text msg_date.
1143
-
1144
- // The notification deliberately carries NO message body — only that a
1145
- // message arrived, from whom, and its id. The body stays in the packet
1146
- // and only leaves through get_messages.
1147
- return transaction::success [
1148
- _notify_agent ($event -> $message_received, $sender_name -> sender_name, $msg_id -> mid, $date -> msg_date),
1149
- _save_state NIL
1150
- ].
1216
+ return a2a_messaging::handle_receive_message text.
1151
1217
  }
1152
1218
 
1153
1219
  // A same-host peer connects via the local contact book. The credential must
@@ -1196,12 +1262,12 @@ application actor loads libraries
1196
1262
  abort "Too many concurrent introductions; try again shortly." when (_count seen_nonces|) >= seen_nonce_cap.
1197
1263
  seen_nonces (intro $nonce) -> now.
1198
1264
 
1199
- existing = contacts sender_id.
1265
+ existing = a2a_messaging::contacts sender_id.
1200
1266
  if existing != NIL
1201
1267
  {
1202
1268
  // Already a contact (idempotent re-introduction): keep my assigned
1203
1269
  // name, refresh the stored address document, deliver any payload.
1204
- peer_ads sender_id -> joiner_ad.
1270
+ a2a_messaging::peer_ads sender_id -> joiner_ad.
1205
1271
  if text != NIL
1206
1272
  {
1207
1273
  mid = deposit_message sender_id (existing? $name) text? now.
@@ -1215,8 +1281,8 @@ application actor loads libraries
1215
1281
 
1216
1282
  if local_auto_accept
1217
1283
  {
1218
- contacts sender_id -> ($name -> joiner_name, $container_id -> sender_id).
1219
- peer_ads sender_id -> joiner_ad.
1284
+ a2a_messaging::contacts sender_id -> ($name -> joiner_name, $container_id -> sender_id).
1285
+ a2a_messaging::peer_ads sender_id -> joiner_ad.
1220
1286
  if text != NIL
1221
1287
  {
1222
1288
  mid = deposit_message sender_id joiner_name text? now.
@@ -1267,8 +1333,8 @@ application actor loads libraries
1267
1333
  if cert_blob == NIL
1268
1334
  {
1269
1335
  // Sender claims to be my root.
1270
- abort "A certificate-less sibling introduction is only accepted from my root." when delegation_cert == NIL.
1271
- abort "Sender is not my root." when sender_id != (delegation_cert? $c $root_cid).
1336
+ abort "A certificate-less sibling introduction is only accepted from my root." when a2a_messaging::delegation_cert == NIL.
1337
+ abort "Sender is not my root." when sender_id != (a2a_messaging::delegation_cert? $c $root_cid).
1272
1338
  link -> ($root_cid -> sender_id, $root_name -> joiner_name, $role_id -> "").
1273
1339
  }
1274
1340
  else
@@ -1279,14 +1345,14 @@ application actor loads libraries
1279
1345
  abort "Sibling certificate does not match the sender's address document." when (cert $c $role_ad_hash) != (_value_id joiner_ad).
1280
1346
 
1281
1347
  root_name_known is str = "".
1282
- if delegation_cert != NIL
1348
+ if a2a_messaging::delegation_cert != NIL
1283
1349
  {
1284
1350
  // I am a role: the cert must name MY root and verify against the
1285
1351
  // root key material pinned at delegation time.
1286
- abort "My root material is missing — cannot verify sibling certificates." when root_ad == NIL.
1287
- abort "Sibling certificate names a different root." when (cert $c $root_cid) != (delegation_cert? $c $root_cid).
1288
- abort "Sibling certificate was not signed by my root." when key_storage::check_signature_new_container (_value_id (cert $c)) (cert $s) (root_ad? $identity $key_list) != TRUE.
1289
- if root_profile != NIL { root_name_known -> root_profile? $p $name. }
1352
+ abort "My root material is missing — cannot verify sibling certificates." when a2a_messaging::root_ad == NIL.
1353
+ abort "Sibling certificate names a different root." when (cert $c $root_cid) != (a2a_messaging::delegation_cert? $c $root_cid).
1354
+ abort "Sibling certificate was not signed by my root." when key_storage::check_signature_new_container (_value_id (cert $c)) (cert $s) (a2a_messaging::root_ad? $identity $key_list) != TRUE.
1355
+ if a2a_messaging::root_profile != NIL { root_name_known -> a2a_messaging::root_profile? $p $name. }
1290
1356
  }
1291
1357
  else
1292
1358
  {
@@ -1295,18 +1361,18 @@ application actor loads libraries
1295
1361
  abort "Sibling certificate names a different root." when (cert $c $root_cid) != _get_container_id().
1296
1362
  my_ad = address_document::get_my_address_document().
1297
1363
  abort "Sibling certificate was not signed by me." when key_storage::check_signature_new_container (_value_id (cert $c)) (cert $s) (my_ad $identity $key_list) != TRUE.
1298
- root_name_known -> my_name.
1364
+ root_name_known -> a2a_messaging::my_name.
1299
1365
  }
1300
1366
  link -> ($root_cid -> cert $c $root_cid, $root_name -> root_name_known, $role_id -> cert $c $role_id).
1301
1367
  }
1302
1368
 
1303
- existing = contacts sender_id.
1369
+ existing = a2a_messaging::contacts sender_id.
1304
1370
  if existing != NIL
1305
1371
  {
1306
1372
  // Already a contact (idempotent re-introduction): keep my assigned
1307
1373
  // name, refresh the stored material, deliver any payload.
1308
- peer_ads sender_id -> joiner_ad.
1309
- contact_roots sender_id -> link?.
1374
+ a2a_messaging::peer_ads sender_id -> joiner_ad.
1375
+ a2a_messaging::contact_roots sender_id -> link?.
1310
1376
  if text != NIL
1311
1377
  {
1312
1378
  mid = deposit_message sender_id (existing? $name) text? now.
@@ -1318,9 +1384,9 @@ application actor loads libraries
1318
1384
  return transaction::success [ _save_state NIL ].
1319
1385
  }
1320
1386
 
1321
- contacts sender_id -> ($name -> joiner_name, $container_id -> sender_id).
1322
- peer_ads sender_id -> joiner_ad.
1323
- contact_roots sender_id -> link?.
1387
+ a2a_messaging::contacts sender_id -> ($name -> joiner_name, $container_id -> sender_id).
1388
+ a2a_messaging::peer_ads sender_id -> joiner_ad.
1389
+ a2a_messaging::contact_roots sender_id -> link?.
1324
1390
  if text != NIL
1325
1391
  {
1326
1392
  mid = deposit_message sender_id joiner_name text? now.