@adapt-toolkit/a2adapt 0.2.0 → 0.4.0

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.
@@ -1,24 +1,291 @@
1
- // a2adapt messenger packet — application.
1
+ // a2adapt messenger packet.
2
2
  //
3
- // SCAFFOLD STATUS (v0): placeholder. Implemented in workspace task "3. MUFL
4
- // messenger packet", modeled on the reference messenger:
5
- // /home/shakhvit/work/adapt/messenger-demo/mufl_code/actor.mu (full, 290 lines)
6
- // /home/shakhvit/work/adapt/basic-messenger-demo/mufl_code/actor.mu (minimal)
3
+ // One ADAPT node == one of these packets. Two nodes talk directly, peer to
4
+ // peer, through a relay broker (no central server). Messages are end-to-end
5
+ // encrypted; the key exchange is handled for us by the stdlib `encrypted_channel`
6
+ // library — we only ever address peers by their container id.
7
7
  //
8
- // Simplified to 1:1 direct contacts (no chat groups) for v0.
8
+ // User transactions (each backs one MCP tool):
9
+ // set_my_name — set the display name peers see for me
10
+ // generate_invite — make a personal invite blob for a named peer
11
+ // add_contact — join via an invite blob, reply to the inviter
12
+ // send_message — send an e2e-encrypted message to a contact
13
+ // list_contacts — (readonly) my contacts
14
+ // list_incoming_messages — (readonly) my inbox
9
15
  //
10
- // State to hold in the packet:
11
- // - my identity (address_document, via key_storage / get_my_address_document())
12
- // - contacts: container_id ->> ( $name, $address_document )
13
- // - pending_invites: invite_id ->> ( $secret_key, $default_name )
14
- // - inbox: received decrypted messages with sender attribution
16
+ // External transactions (inbound, not exposed as tools):
17
+ // accept_contact — inviter learns the joiner's identity + name
18
+ // receive_message — store a decrypted inbound message
15
19
  //
16
- // Crypto/identity stdlib to use (see /home/shakhvit/work/adapt/mufl3/mufl_stdlib):
17
- // cryptography/key_storage.mm default_encrypt / default_sign / decrypt_message
18
- // _crypto_construct_encryption_keypair, _crypto_encrypt_message, _crypto_decrypt_message
19
- // cryptography/address_document.mm, address_document_types.mm
20
- // transactions/transaction.mm transaction::encrypt, transaction::action::send
21
- // mufl_stdlib/current_transaction_info.mm validate_origin_or_abort, is_encrypted, is_signed
22
- //
23
- // Named-invite UX: generate_invite embeds $default_name in the invite payload;
24
- // add_contact uses a custom name if supplied, else the embedded default.
20
+ // Naming model (personal invites): generate_invite('Bob') tags a pending invite
21
+ // with the peer-name "Bob"; whoever redeems it is registered under "Bob" (the
22
+ // inviter's assigned name wins). The joiner, in turn, sees the inviter under the
23
+ // inviter's own self-name (set via set_my_name), unless the joiner overrides it.
24
+
25
+ application actor loads libraries
26
+ identity_proof_document,
27
+ attestation_document,
28
+ native_attestation_document,
29
+ transaction_message_decoder,
30
+ address_document,
31
+ address_document_types,
32
+ key_utils,
33
+ key_storage,
34
+ continuation,
35
+ encrypted_channel,
36
+ current_transaction_info
37
+ uses transactions
38
+ {
39
+ hidden
40
+ {
41
+ metadef contact_t: ($name -> str, $container_id -> global_id).
42
+ metadef invite_t: ($invite_id -> global_id, $inviter_id -> global_id, $inviter_name -> str, $inviter_ad -> address_document_types::t_address_document).
43
+ metadef message_t: ($sender_id -> global_id, $sender_name -> str, $text -> str, $date -> time).
44
+
45
+ // Wire the deserialization primitive into the libraries that need it.
46
+ _read_or_abort = grab( _read_or_abort ).
47
+ key_storage::init ($_read_or_abort -> _read_or_abort).
48
+ encrypted_channel::init ($_read_or_abort -> _read_or_abort).
49
+
50
+ // ---- packet state ---------------------------------------------------
51
+ // The display name peers see for me (set via set_my_name).
52
+ my_name is str = "".
53
+ // Known contacts, keyed by their container id.
54
+ contacts is (global_id ->> contact_t) = (,).
55
+ // Invites I generated, keyed by invite id -> the name I assigned the peer.
56
+ pending_invites is (global_id ->> str) = (,).
57
+ // Received messages, append-only (the host tracks how many it surfaced).
58
+ inbox is message_t[] = [].
59
+ // Peer address documents, captured when a contact is established. These are
60
+ // self-signed, code-independent, and seed-stable, so on a code upgrade the
61
+ // host re-creates this packet from the same seed and import_state replays
62
+ // them through address_document::process_address_document — re-registering
63
+ // every peer's keys in key_storage so encrypted channels survive the upgrade
64
+ // with no re-handshake. Only peer PUBLIC keys travel here, never secrets.
65
+ peer_ads is (global_id ->> address_document_types::t_address_document) = (,).
66
+
67
+ // Signal the host to persist the packet. Only emitted at the end of a
68
+ // complete procedure — intermediate states (e.g. channel handshake) are
69
+ // never saved, so a crash mid-handshake restores to the last stable point.
70
+ fn _save_state (_) = (transaction::action::return_data ($kind -> $save_state)).
71
+ fn _return_data (payload: any) = (transaction::action::return_data ($kind -> $data, $payload -> payload)).
72
+ fn _notify_agent (payload: any) = (transaction::action::return_data ($kind -> $notify_agent, $payload -> payload)).
73
+
74
+ // Resolve a contact reference (a display name or stringified container id)
75
+ // to a container id; aborts if no contact matches.
76
+ fn resolve_contact (ref: str) -> global_id
77
+ {
78
+ found is global_id+ = NIL.
79
+ sc contacts -- (cid -> c) ?? found == NIL && ((c $name) == ref || (_str cid) == ref)
80
+ {
81
+ found -> cid.
82
+ }
83
+ abort "Unknown contact: " + ref when found == NIL.
84
+ return found?.
85
+ }
86
+ }
87
+
88
+ // ---- user transactions --------------------------------------------------
89
+
90
+ trn set_my_name _:($name -> name: str)
91
+ {
92
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
93
+ my_name -> name.
94
+ return transaction::success [
95
+ _return_data ($name -> name),
96
+ _save_state NIL
97
+ ].
98
+ }
99
+
100
+ trn generate_invite _:($name -> name: str)
101
+ {
102
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
103
+
104
+ invite_id = _new_id "a2adapt invite".
105
+ // Remember the name I'm assigning to whoever redeems this invite.
106
+ pending_invites invite_id -> name.
107
+
108
+ invite is invite_t = (
109
+ $invite_id -> invite_id,
110
+ $inviter_id -> _get_container_id(),
111
+ $inviter_name -> my_name,
112
+ // Embed my address document so the joiner can store it and re-register
113
+ // me after a code upgrade (see peer_ads / import_state).
114
+ $inviter_ad -> address_document::get_my_address_document()
115
+ ).
116
+
117
+ return transaction::success [
118
+ _return_data (
119
+ $invite -> (_write invite),
120
+ $invite_id -> invite_id,
121
+ $peer_name -> name
122
+ ),
123
+ _save_state NIL
124
+ ].
125
+ }
126
+
127
+ trn add_contact _:($invite -> invite_blob: bin, $name -> custom_name: str+)
128
+ {
129
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
130
+
131
+ invite = (_read_or_abort invite_blob) safe invite_t.
132
+ inviter_id = invite $inviter_id.
133
+ abort "This invite is your own — you cannot add yourself." when inviter_id == _get_container_id().
134
+
135
+ // Register the inviter under my chosen name, or the name they embedded.
136
+ contact_name = (custom_name == NIL ?? (invite $inviter_name) ; custom_name?).
137
+ contacts inviter_id -> ($name -> contact_name, $container_id -> inviter_id).
138
+ // Remember the inviter's address document so I can re-register them after
139
+ // an upgrade (their keys are seed-stable, so this stays valid).
140
+ peer_ads inviter_id -> invite $inviter_ad.
141
+
142
+ invite_id = invite $invite_id.
143
+ my_self_name = my_name.
144
+ my_ad = address_document::get_my_address_document().
145
+
146
+ // Establish the encrypted channel with the inviter (handshake happens
147
+ // transparently if we haven't talked before), then tell them I redeemed
148
+ // their invite, what my own name is, and my address document, so they can
149
+ // finish the handshake and remember me for future upgrades.
150
+ return encrypted_channel::execute_transaction inviter_id (fn (_) -> transaction::results::type {
151
+ return transaction::success [
152
+ encrypted_channel::send_encrypted_tx inviter_id (
153
+ $name -> "::actor::accept_contact",
154
+ $targ -> ($invite_id -> invite_id, $joiner_name -> my_self_name, $joiner_ad -> my_ad)
155
+ ),
156
+ _return_data ($added -> contact_name, $container_id -> inviter_id),
157
+ _save_state NIL
158
+ ].
159
+ }).
160
+ }
161
+
162
+ trn send_message _:($contact -> contact_ref: str, $text -> text: str)
163
+ {
164
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
165
+
166
+ target_id = resolve_contact contact_ref.
167
+
168
+ return encrypted_channel::execute_transaction target_id (fn (_) -> transaction::results::type {
169
+ return transaction::success [
170
+ encrypted_channel::send_encrypted_tx target_id (
171
+ $name -> "::actor::receive_message",
172
+ $targ -> ($text -> text)
173
+ ),
174
+ _return_data ($sent_to -> target_id, $text -> text),
175
+ _save_state NIL
176
+ ].
177
+ }).
178
+ }
179
+
180
+ trn readonly list_contacts _
181
+ {
182
+ return contacts.
183
+ }
184
+
185
+ trn readonly list_incoming_messages _
186
+ {
187
+ return inbox.
188
+ }
189
+
190
+ // ---- upgrade: state export / import -------------------------------------
191
+ // The host persists state by calling export_state (readonly) and serializing
192
+ // the returned value to a code-independent blob. On a code upgrade it recreates
193
+ // this packet from the same seed (same container id + same default keys, since
194
+ // both derive from the seed) and replays the blob through import_state.
195
+ //
196
+ // The packet-level snapshot is NOT used for upgrades: it is bound to the unit
197
+ // code hash, so a new .muflo cannot load an old snapshot. This data blob is.
198
+
199
+ trn readonly export_state _
200
+ {
201
+ return (
202
+ $my_name -> my_name,
203
+ $contacts -> contacts,
204
+ $pending_invites -> pending_invites,
205
+ $inbox -> inbox,
206
+ $peer_ads -> peer_ads
207
+ ).
208
+ }
209
+
210
+ trn import_state data: any
211
+ {
212
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::user,).
213
+
214
+ d = data safe (
215
+ $my_name -> str,
216
+ $contacts -> (global_id ->> contact_t),
217
+ $pending_invites -> (global_id ->> str),
218
+ $inbox -> message_t[],
219
+ $peer_ads -> (global_id ->> address_document_types::t_address_document)
220
+ ).
221
+
222
+ my_name -> d $my_name.
223
+ contacts -> d $contacts.
224
+ pending_invites -> d $pending_invites.
225
+ inbox -> d $inbox.
226
+ peer_ads -> d $peer_ads.
227
+
228
+ // Re-register every peer's keys so encrypted channels keep working after
229
+ // the upgrade — no handshake needed (my own keys are unchanged, and the
230
+ // peers' self-signed address documents re-authorize on this fresh packet).
231
+ sc peer_ads -- ( -> ad)
232
+ {
233
+ address_document::process_address_document ad TRUE.
234
+ }
235
+
236
+ return transaction::success [
237
+ _return_data ($imported -> TRUE, $contacts -> _count contacts|, $peers -> _count peer_ads|),
238
+ _save_state NIL
239
+ ].
240
+ }
241
+
242
+ // ---- external (inbound) transactions ------------------------------------
243
+
244
+ trn accept_contact _:($invite_id -> invite_id: global_id, $joiner_name -> joiner_name: str, $joiner_ad -> joiner_ad: address_document_types::t_address_document)
245
+ {
246
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::external,).
247
+ encrypted_channel::check_encrypted_or_abort().
248
+
249
+ sender_id = current_transaction_info::get_external_envelope_or_abort() $from.
250
+
251
+ // The name I assigned when I generated the invite wins; fall back to the
252
+ // joiner's self-name if this invite is unknown (shouldn't happen).
253
+ assigned_name = pending_invites invite_id.
254
+ contact_name = (assigned_name == NIL ?? joiner_name ; assigned_name?).
255
+
256
+ contacts sender_id -> ($name -> contact_name, $container_id -> sender_id).
257
+ // Remember the joiner's address document for upgrade-time re-registration.
258
+ peer_ads sender_id -> joiner_ad.
259
+ if pending_invites invite_id != NIL { delete pending_invites invite_id. }
260
+
261
+ return transaction::success [
262
+ _notify_agent ($event -> $contact_accepted, $name -> contact_name, $container_id -> sender_id),
263
+ _save_state NIL
264
+ ].
265
+ }
266
+
267
+ trn receive_message _:($text -> text: str)
268
+ {
269
+ current_transaction_info::validate_origin_or_abort (transaction::envelope::origin::external,).
270
+ encrypted_channel::check_encrypted_or_abort().
271
+
272
+ sender_id = current_transaction_info::get_external_envelope_or_abort() $from.
273
+ sender = contacts sender_id.
274
+ abort "Message from an unknown sender was rejected." when sender == NIL.
275
+
276
+ sender_name = sender? $name.
277
+ msg_date = current_transaction_info::get_transaction_time().
278
+
279
+ inbox (_count inbox|) -> (
280
+ $sender_id -> sender_id,
281
+ $sender_name -> sender_name,
282
+ $text -> text,
283
+ $date -> msg_date
284
+ ).
285
+
286
+ return transaction::success [
287
+ _notify_agent ($event -> $message_received, $sender_name -> sender_name, $text -> text, $date -> msg_date),
288
+ _save_state NIL
289
+ ].
290
+ }
291
+ }
@@ -1,19 +1,22 @@
1
- // a2adapt messenger packet — build configuration.
1
+ // a2adapt messenger packet — compile configuration.
2
2
  //
3
- // Loads the ADAPT standard library and declares the application entry module
4
- // (actor.mu) plus the exported user transactions that the MCP server invokes.
3
+ // Pulls in the full ADAPT standard library so `actor.mu` can `loads libraries`
4
+ // the crypto / identity / transport modules by name. No app-private libraries
5
+ // (everything we need lives in the stdlib, including `encrypted_channel`, which
6
+ // does the peer key-exchange for us).
5
7
  //
6
- // SCAFFOLD STATUS (v0): placeholder. The real config + actor are written in
7
- // workspace task "3. MUFL messenger packet", modeled on
8
- // /home/shakhvit/work/adapt/messenger-demo/mufl_code/{config.mufl,actor.mu}.
9
- //
10
- // Expected exported transactions (each maps to one MCP tool):
11
- // ::actor::generate_invite (name) -> invite blob
12
- // ::actor::add_contact (invite, name?) -> contact registered
13
- // ::actor::accept_contact (inbound) -> reciprocal registration (TOFU)
14
- // ::actor::send_message (contact, text) -> encrypted send
15
- // ::actor::receive_message (inbound) -> append to inbox
16
- // ::actor::list_contacts () -> contacts
17
- // ::actor::list_incoming_messages () -> inbox
18
- //
19
- // Compile (task 3): mufl-compile -f config.mufl -repo <out> -> <hash>.muflo
8
+ // Compile:
9
+ // MUFL_STDLIB_PATH=<adapt-toolkit>/mufl_stdlib \
10
+ // mufl-compile -mp <adapt-toolkit>/meta -mp <adapt-toolkit>/transactions -d-c actor.mu
11
+ // -> emits <content-hash>.muflo in the cwd.
12
+
13
+ config script
14
+ {
15
+ (
16
+ $imports -> ((config_load #$MUFL_STDLIB_PATH) $exports),
17
+ $exports -> (
18
+ $libraries -> (,),
19
+ $applications -> (,)
20
+ )
21
+ ).
22
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@adapt-toolkit/a2adapt",
3
- "version": "0.2.0",
4
- "description": "MCP server for a2adapt — a native ADAPT node hosting one packet, exposing secure agent-to-agent messaging tools over stdio.",
3
+ "version": "0.4.0",
4
+ "description": "MCP server daemon for a2adapt — one native ADAPT wrapper hosting N self-sovereign identities, exposing secure agent-to-agent messaging tools over HTTP (Streamable HTTP). Run `a2adapt-mcp start`.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Adapt Toolkit",
@@ -14,9 +14,17 @@
14
14
  "bugs": {
15
15
  "url": "https://github.com/adapt-toolkit/a2adapt/issues"
16
16
  },
17
- "keywords": ["mcp", "model-context-protocol", "claude", "a2a", "adapt", "e2e", "messaging"],
17
+ "keywords": [
18
+ "mcp",
19
+ "model-context-protocol",
20
+ "claude",
21
+ "a2a",
22
+ "adapt",
23
+ "e2e",
24
+ "messaging"
25
+ ],
18
26
  "bin": {
19
- "a2adapt-mcp": "dist/index.js"
27
+ "a2adapt-mcp": "dist/cli.js"
20
28
  },
21
29
  "files": [
22
30
  "dist",
@@ -25,14 +33,25 @@
25
33
  "publishConfig": {
26
34
  "access": "public"
27
35
  },
36
+ "engines": {
37
+ "node": ">=20"
38
+ },
28
39
  "scripts": {
29
40
  "build": "node build.mjs",
30
41
  "build:tsc": "tsc -p tsconfig.json",
31
- "dev": "tsx src/index.ts",
32
- "start": "node dist/index.js",
42
+ "dev": "tsx src/cli.ts serve",
43
+ "dev:stdio": "A2ADAPT_TRANSPORT=stdio tsx src/index.ts",
44
+ "start": "node dist/cli.js start",
45
+ "stop": "node dist/cli.js stop",
46
+ "status": "node dist/cli.js status",
47
+ "serve": "node dist/cli.js serve",
33
48
  "prepublishOnly": "node build.mjs",
34
49
  "typecheck": "tsc -p tsconfig.json --noEmit"
35
50
  },
51
+ "dependencies": {
52
+ "@adapt-toolkit/sdk": "^0.2.2",
53
+ "@adapt-toolkit/sdk-native": "^0.2.1"
54
+ },
36
55
  "devDependencies": {
37
56
  "@modelcontextprotocol/sdk": "^1.0.4",
38
57
  "@types/node": "^20.14.0",