@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.
- package/dist/cli.js +422 -0
- package/dist/hooks/runner.js +115 -17
- package/dist/index.js +2050 -89
- package/dist/mufl_code/A93F52302D67D1F28269D3F35657610A0FA140AABC521D667A262C101A7AC090.muflo +0 -0
- package/dist/mufl_code/actor.mu +287 -20
- package/dist/mufl_code/config.mufl +20 -17
- package/package.json +25 -6
|
Binary file
|
package/dist/mufl_code/actor.mu
CHANGED
|
@@ -1,24 +1,291 @@
|
|
|
1
|
-
// a2adapt messenger packet
|
|
1
|
+
// a2adapt messenger packet.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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
|
-
//
|
|
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
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
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
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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 —
|
|
1
|
+
// a2adapt messenger packet — compile configuration.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
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
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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.
|
|
4
|
-
"description": "MCP server for a2adapt —
|
|
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": [
|
|
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/
|
|
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/
|
|
32
|
-
"
|
|
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",
|