ai2ai 0.1.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/README.md +184 -0
- package/bin/ai2ai.js +87 -0
- package/lib/approve.js +137 -0
- package/lib/config.js +120 -0
- package/lib/connect.js +89 -0
- package/lib/contacts.js +62 -0
- package/lib/crypto.js +124 -0
- package/lib/init.js +148 -0
- package/lib/pending.js +114 -0
- package/lib/protocol.js +161 -0
- package/lib/send.js +135 -0
- package/lib/start.js +318 -0
- package/lib/status.js +70 -0
- package/package.json +37 -0
package/lib/start.js
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai2ai start — Start the AI2AI HTTP server
|
|
3
|
+
* Runs the protocol server on the configured port.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
'use strict';
|
|
7
|
+
|
|
8
|
+
const http = require('http');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const crypto = require('crypto');
|
|
12
|
+
const { requireConfig, loadContacts, saveContacts, PENDING_DIR, CONVERSATIONS_DIR } = require('./config');
|
|
13
|
+
const { loadOrCreateKeys, getFingerprint, signMessage, verifyMessage } = require('./crypto');
|
|
14
|
+
|
|
15
|
+
// Rate limiting
|
|
16
|
+
const rateLimiter = new Map();
|
|
17
|
+
const RATE_LIMIT = 20;
|
|
18
|
+
const RATE_WINDOW = 60000;
|
|
19
|
+
|
|
20
|
+
function checkRateLimit(agentId) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
const window = rateLimiter.get(agentId) || [];
|
|
23
|
+
const recent = window.filter(t => now - t < RATE_WINDOW);
|
|
24
|
+
if (recent.length >= RATE_LIMIT) return false;
|
|
25
|
+
recent.push(now);
|
|
26
|
+
rateLimiter.set(agentId, recent);
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Save a pending message for human review
|
|
32
|
+
*/
|
|
33
|
+
function savePending(id, data) {
|
|
34
|
+
const pendingPath = path.join(PENDING_DIR, `${id}.json`);
|
|
35
|
+
fs.writeFileSync(pendingPath, JSON.stringify(data, null, 2));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Save to conversation history
|
|
40
|
+
*/
|
|
41
|
+
function saveToConversation(conversationId, message) {
|
|
42
|
+
const convPath = path.join(CONVERSATIONS_DIR, `${conversationId}.jsonl`);
|
|
43
|
+
fs.appendFileSync(convPath, JSON.stringify(message) + '\n');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format incoming message for human display
|
|
48
|
+
*/
|
|
49
|
+
function formatForHuman(envelope) {
|
|
50
|
+
const from = envelope.from?.human || envelope.from?.agent || 'Unknown';
|
|
51
|
+
const intent = envelope.intent || envelope.type;
|
|
52
|
+
|
|
53
|
+
if (envelope.intent === 'schedule.meeting') {
|
|
54
|
+
const p = envelope.payload;
|
|
55
|
+
const times = (p.proposed_times || [])
|
|
56
|
+
.map((t, i) => ` ${i + 1}. ${new Date(t).toLocaleString()}`)
|
|
57
|
+
.join('\n');
|
|
58
|
+
return [
|
|
59
|
+
`📅 Meeting Request from ${from}`,
|
|
60
|
+
` Subject: ${p.subject || 'No subject'}`,
|
|
61
|
+
` Times:\n${times}`,
|
|
62
|
+
` Duration: ${p.duration_minutes || 60} min`,
|
|
63
|
+
p.location_preference ? ` Location: ${p.location_preference}` : '',
|
|
64
|
+
p.notes ? ` Notes: ${p.notes}` : '',
|
|
65
|
+
].filter(Boolean).join('\n');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (envelope.intent === 'message.relay') {
|
|
69
|
+
const p = envelope.payload;
|
|
70
|
+
const emoji = p.urgency === 'high' ? '🔴' : p.urgency === 'medium' ? '🟡' : '💬';
|
|
71
|
+
return [
|
|
72
|
+
`${emoji} Message from ${from}:`,
|
|
73
|
+
` "${p.message}"`,
|
|
74
|
+
p.reply_requested ? ' (Reply requested)' : '',
|
|
75
|
+
].filter(Boolean).join('\n');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (envelope.intent === 'info.request') {
|
|
79
|
+
return [
|
|
80
|
+
`❓ Question from ${from}:`,
|
|
81
|
+
` "${envelope.payload.question}"`,
|
|
82
|
+
envelope.payload.context ? ` Context: ${envelope.payload.context}` : '',
|
|
83
|
+
].filter(Boolean).join('\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (envelope.type === 'response') {
|
|
87
|
+
return `💬 Response from ${from}:\n ${JSON.stringify(envelope.payload, null, 2)}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (envelope.type === 'confirm') {
|
|
91
|
+
return `✅ Confirmed by ${from}: ${JSON.stringify(envelope.payload)}`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (envelope.type === 'reject') {
|
|
95
|
+
return `❌ Declined by ${from}: ${envelope.payload?.reason || 'No reason given'}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return `📨 ${intent || 'Message'} from ${from}:\n ${JSON.stringify(envelope.payload, null, 2)}`;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Handle a ping (handshake)
|
|
103
|
+
*/
|
|
104
|
+
function handlePing(envelope, config) {
|
|
105
|
+
const keys = loadOrCreateKeys();
|
|
106
|
+
const fromAgent = envelope.from?.agent;
|
|
107
|
+
|
|
108
|
+
// Store their info as a contact
|
|
109
|
+
if (fromAgent && fromAgent !== 'unknown') {
|
|
110
|
+
const contacts = loadContacts();
|
|
111
|
+
contacts[fromAgent] = {
|
|
112
|
+
...contacts[fromAgent],
|
|
113
|
+
humanName: envelope.from?.human,
|
|
114
|
+
publicKey: envelope.payload?.public_key,
|
|
115
|
+
capabilities: envelope.payload?.capabilities,
|
|
116
|
+
timezone: envelope.payload?.timezone,
|
|
117
|
+
trustLevel: contacts[fromAgent]?.trustLevel || 'none',
|
|
118
|
+
lastSeen: new Date().toISOString(),
|
|
119
|
+
};
|
|
120
|
+
saveContacts(contacts);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
status: 'ok',
|
|
125
|
+
type: 'ping',
|
|
126
|
+
payload: {
|
|
127
|
+
capabilities: [
|
|
128
|
+
'schedule.meeting', 'schedule.call', 'message.relay',
|
|
129
|
+
'info.request', 'info.share', 'social.introduction',
|
|
130
|
+
],
|
|
131
|
+
languages: ['en'],
|
|
132
|
+
timezone: config.timezone || 'UTC',
|
|
133
|
+
model_info: 'local',
|
|
134
|
+
protocol_versions: ['0.1'],
|
|
135
|
+
public_key: keys.publicKey,
|
|
136
|
+
fingerprint: getFingerprint(keys.publicKey),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Process an incoming message
|
|
143
|
+
*/
|
|
144
|
+
function processMessage(envelope, config) {
|
|
145
|
+
const fromAgent = envelope.from?.agent;
|
|
146
|
+
|
|
147
|
+
// Check rate limit
|
|
148
|
+
if (!checkRateLimit(fromAgent)) {
|
|
149
|
+
return { status: 'rejected', reason: 'rate_limited' };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Verify signature if we have their public key
|
|
153
|
+
const contacts = loadContacts();
|
|
154
|
+
const contact = contacts[fromAgent];
|
|
155
|
+
if (contact?.publicKey && envelope.signature) {
|
|
156
|
+
const valid = verifyMessage(envelope, envelope.signature, contact.publicKey);
|
|
157
|
+
if (!valid) {
|
|
158
|
+
console.log(` ⚠️ Invalid signature from ${fromAgent}`);
|
|
159
|
+
return { status: 'rejected', reason: 'invalid_signature' };
|
|
160
|
+
}
|
|
161
|
+
console.log(` ✅ Signature verified for ${fromAgent}`);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Save to conversation
|
|
165
|
+
saveToConversation(envelope.conversation, envelope);
|
|
166
|
+
|
|
167
|
+
// Handle by type
|
|
168
|
+
if (envelope.type === 'ping') {
|
|
169
|
+
return handlePing(envelope, config);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Everything else goes to pending for human review
|
|
173
|
+
const humanMessage = formatForHuman(envelope);
|
|
174
|
+
savePending(envelope.id, {
|
|
175
|
+
envelope,
|
|
176
|
+
approvalMessage: humanMessage,
|
|
177
|
+
handler: envelope.intent || envelope.type,
|
|
178
|
+
createdAt: new Date().toISOString(),
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
console.log(`\n 📥 New message from ${fromAgent}:`);
|
|
182
|
+
console.log(` ${humanMessage.split('\n')[0]}`);
|
|
183
|
+
console.log(' Run `ai2ai pending` to review.\n');
|
|
184
|
+
|
|
185
|
+
return {
|
|
186
|
+
status: 'pending_approval',
|
|
187
|
+
message: 'Message received. Waiting for human approval.',
|
|
188
|
+
conversation: envelope.conversation,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function run() {
|
|
193
|
+
const config = requireConfig();
|
|
194
|
+
const keys = loadOrCreateKeys();
|
|
195
|
+
const fingerprint = getFingerprint(keys.publicKey);
|
|
196
|
+
|
|
197
|
+
const server = http.createServer((req, res) => {
|
|
198
|
+
// CORS
|
|
199
|
+
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
200
|
+
res.setHeader('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
|
|
201
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-AI2AI-Version');
|
|
202
|
+
|
|
203
|
+
if (req.method === 'OPTIONS') {
|
|
204
|
+
res.writeHead(204);
|
|
205
|
+
res.end();
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// GET /ai2ai/health
|
|
210
|
+
if (req.method === 'GET' && req.url === '/ai2ai/health') {
|
|
211
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
212
|
+
res.end(JSON.stringify({
|
|
213
|
+
status: 'online',
|
|
214
|
+
protocol: 'ai2ai',
|
|
215
|
+
version: '0.1',
|
|
216
|
+
agent: config.agentName,
|
|
217
|
+
}));
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// GET /.well-known/ai2ai.json
|
|
222
|
+
if (req.method === 'GET' && req.url === '/.well-known/ai2ai.json') {
|
|
223
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
224
|
+
res.end(JSON.stringify({
|
|
225
|
+
ai2ai: '0.1',
|
|
226
|
+
agent: config.agentName,
|
|
227
|
+
human: config.humanName,
|
|
228
|
+
endpoint: `http://localhost:${config.port}/ai2ai`,
|
|
229
|
+
public_key: keys.publicKey,
|
|
230
|
+
fingerprint,
|
|
231
|
+
capabilities: [
|
|
232
|
+
'schedule.meeting', 'schedule.call', 'message.relay',
|
|
233
|
+
'info.request', 'info.share', 'social.introduction',
|
|
234
|
+
],
|
|
235
|
+
}, null, 2));
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// POST /ai2ai
|
|
240
|
+
if (req.method === 'POST' && req.url === '/ai2ai') {
|
|
241
|
+
let body = '';
|
|
242
|
+
req.on('data', chunk => {
|
|
243
|
+
body += chunk;
|
|
244
|
+
if (body.length > 102400) {
|
|
245
|
+
res.writeHead(413, { 'Content-Type': 'application/json' });
|
|
246
|
+
res.end(JSON.stringify({ error: 'Payload too large' }));
|
|
247
|
+
req.destroy();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
req.on('end', () => {
|
|
252
|
+
let envelope;
|
|
253
|
+
try {
|
|
254
|
+
envelope = JSON.parse(body);
|
|
255
|
+
} catch {
|
|
256
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
257
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (!envelope.ai2ai || !envelope.type || !envelope.from) {
|
|
262
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
263
|
+
res.end(JSON.stringify({ error: 'Invalid AI2AI envelope' }));
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const result = processMessage(envelope, config);
|
|
269
|
+
const statusCode = result.status === 'rejected' ? 403 : 200;
|
|
270
|
+
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
|
271
|
+
res.end(JSON.stringify(result));
|
|
272
|
+
} catch (err) {
|
|
273
|
+
console.error(` ❌ Error processing message: ${err.message}`);
|
|
274
|
+
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
275
|
+
res.end(JSON.stringify({ error: 'Internal error' }));
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 404
|
|
282
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
283
|
+
res.end(JSON.stringify({ error: 'Not found. POST to /ai2ai' }));
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
server.listen(config.port, () => {
|
|
287
|
+
const contacts = loadContacts();
|
|
288
|
+
const contactCount = Object.keys(contacts).length;
|
|
289
|
+
|
|
290
|
+
console.log(`
|
|
291
|
+
╔══════════════════════════════════════════╗
|
|
292
|
+
║ 🦞 AI2AI Server Running ║
|
|
293
|
+
╚══════════════════════════════════════════╝
|
|
294
|
+
|
|
295
|
+
🤖 Agent: ${config.agentName}
|
|
296
|
+
👤 Human: ${config.humanName}
|
|
297
|
+
🌐 Endpoint: http://localhost:${config.port}/ai2ai
|
|
298
|
+
🏥 Health: http://localhost:${config.port}/ai2ai/health
|
|
299
|
+
🔑 Fingerprint: ${fingerprint}
|
|
300
|
+
👥 Contacts: ${contactCount}
|
|
301
|
+
|
|
302
|
+
Waiting for incoming messages...
|
|
303
|
+
Press Ctrl+C to stop.
|
|
304
|
+
`);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
// Graceful shutdown
|
|
308
|
+
process.on('SIGINT', () => {
|
|
309
|
+
console.log('\n\n 👋 Shutting down AI2AI server...\n');
|
|
310
|
+
server.close(() => process.exit(0));
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
process.on('SIGTERM', () => {
|
|
314
|
+
server.close(() => process.exit(0));
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = { run };
|
package/lib/status.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ai2ai status — Show server status, identity, and diagnostics
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
'use strict';
|
|
6
|
+
|
|
7
|
+
const { loadConfig, loadContacts, PENDING_DIR, AI2AI_DIR } = require('./config');
|
|
8
|
+
const { loadKeys, getFingerprint } = require('./crypto');
|
|
9
|
+
const { fetchGet } = require('./protocol');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
|
|
12
|
+
async function run() {
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
const keys = loadKeys();
|
|
15
|
+
|
|
16
|
+
console.log(`
|
|
17
|
+
╔══════════════════════════════════════════╗
|
|
18
|
+
║ 🦞 AI2AI Status ║
|
|
19
|
+
╚══════════════════════════════════════════╝
|
|
20
|
+
`);
|
|
21
|
+
|
|
22
|
+
// Config
|
|
23
|
+
if (!config) {
|
|
24
|
+
console.log(' ⚠️ Not configured. Run `ai2ai init` to set up.\n');
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const fingerprint = keys ? getFingerprint(keys.publicKey) : '(no keys)';
|
|
29
|
+
const contacts = loadContacts();
|
|
30
|
+
const contactCount = Object.keys(contacts).length;
|
|
31
|
+
|
|
32
|
+
// Count pending
|
|
33
|
+
let pendingCount = 0;
|
|
34
|
+
try {
|
|
35
|
+
if (fs.existsSync(PENDING_DIR)) {
|
|
36
|
+
pendingCount = fs.readdirSync(PENDING_DIR).filter(f => f.endsWith('.json')).length;
|
|
37
|
+
}
|
|
38
|
+
} catch { /* ignore */ }
|
|
39
|
+
|
|
40
|
+
console.log(` 👤 Human: ${config.humanName}`);
|
|
41
|
+
console.log(` 🤖 Agent: ${config.agentName}`);
|
|
42
|
+
console.log(` 🌐 Port: ${config.port}`);
|
|
43
|
+
console.log(` 🕐 Timezone: ${config.timezone || 'Not set'}`);
|
|
44
|
+
console.log(` 🔑 Fingerprint: ${fingerprint}`);
|
|
45
|
+
console.log(` 👥 Contacts: ${contactCount}`);
|
|
46
|
+
console.log(` 📬 Pending: ${pendingCount}`);
|
|
47
|
+
console.log(` 📁 Config dir: ${AI2AI_DIR}`);
|
|
48
|
+
|
|
49
|
+
if (config.telegramToken) {
|
|
50
|
+
console.log(` 📱 Telegram: Configured`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if server is running
|
|
54
|
+
console.log(`\n 🏥 Server health check...`);
|
|
55
|
+
try {
|
|
56
|
+
const result = await fetchGet(`http://localhost:${config.port}/ai2ai/health`);
|
|
57
|
+
if (result.data?.status === 'online') {
|
|
58
|
+
console.log(` ✅ Server is running on port ${config.port}`);
|
|
59
|
+
} else {
|
|
60
|
+
console.log(` ⚠️ Server responded but status is: ${result.data?.status || 'unknown'}`);
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
console.log(` ❌ Server is not running on port ${config.port}`);
|
|
64
|
+
console.log(` Start it with: ai2ai start`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log('');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = { run };
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ai2ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The open protocol for AI agents to talk to each other",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Darren Edwards",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ai2ai": "./bin/ai2ai.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"lib/",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"ai",
|
|
20
|
+
"agent",
|
|
21
|
+
"protocol",
|
|
22
|
+
"ai2ai",
|
|
23
|
+
"openclaw",
|
|
24
|
+
"decentralized",
|
|
25
|
+
"agent-to-agent",
|
|
26
|
+
"multi-agent",
|
|
27
|
+
"communication"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/DarrenEdwards111/ai2ai-protocol"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/DarrenEdwards111/ai2ai-protocol#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/DarrenEdwards111/ai2ai-protocol/issues"
|
|
36
|
+
}
|
|
37
|
+
}
|