ai2ai 0.1.0 → 1.0.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.
Files changed (95) hide show
  1. package/.keys/agent.key +3 -0
  2. package/.keys/agent.pub +3 -0
  3. package/.keys/x25519.key.der +0 -0
  4. package/.keys/x25519.pub.der +0 -0
  5. package/SKILL.md +39 -0
  6. package/ai2ai-client.js +351 -0
  7. package/ai2ai-conversations.js +324 -0
  8. package/{lib/crypto.js → ai2ai-crypto.js} +19 -39
  9. package/ai2ai-discovery.js +398 -0
  10. package/ai2ai-encryption.js +292 -0
  11. package/ai2ai-handlers.js +392 -0
  12. package/ai2ai-logger.js +148 -0
  13. package/ai2ai-queue.js +281 -0
  14. package/ai2ai-server.js +433 -0
  15. package/ai2ai-trust.js +137 -0
  16. package/client.js +434 -0
  17. package/contacts.example.json +45 -0
  18. package/contacts.json +57 -0
  19. package/conversations/06dfc9fc-e0fb-47a6-80ea-6f89b805dcc9.jsonl +1 -0
  20. package/conversations/06dfc9fc-e0fb-47a6-80ea-6f89b805dcc9.meta.json +31 -0
  21. package/conversations/132889ee-3c68-4a86-a465-829c467f6782.jsonl +1 -0
  22. package/conversations/132889ee-3c68-4a86-a465-829c467f6782.meta.json +27 -0
  23. package/conversations/16c99cf3-7250-4136-8d4a-f5214bcd32ba.jsonl +1 -0
  24. package/conversations/16c99cf3-7250-4136-8d4a-f5214bcd32ba.meta.json +27 -0
  25. package/conversations/3f62daf5-49cb-4f9b-9d25-de70625f46e2.jsonl +1 -0
  26. package/conversations/3f62daf5-49cb-4f9b-9d25-de70625f46e2.meta.json +31 -0
  27. package/conversations/532b39ab-d513-4e40-98ff-2d3df2d5f256.jsonl +1 -0
  28. package/conversations/532b39ab-d513-4e40-98ff-2d3df2d5f256.meta.json +27 -0
  29. package/conversations/5549dc7a-d62e-49f6-93b6-977da7908a11.jsonl +1 -0
  30. package/conversations/5549dc7a-d62e-49f6-93b6-977da7908a11.meta.json +27 -0
  31. package/conversations/610ab34a-c7f4-4d24-98b1-75e3f98835d3.jsonl +1 -0
  32. package/conversations/610ab34a-c7f4-4d24-98b1-75e3f98835d3.meta.json +31 -0
  33. package/conversations/611610be-28f2-4b31-9afc-b5e278334d8a.jsonl +1 -0
  34. package/conversations/611610be-28f2-4b31-9afc-b5e278334d8a.meta.json +27 -0
  35. package/conversations/69ed7660-73bc-4994-9070-db4b264ccd18.jsonl +1 -0
  36. package/conversations/69ed7660-73bc-4994-9070-db4b264ccd18.meta.json +27 -0
  37. package/conversations/85eb0bf1-8a1a-4e9b-8b98-963f6b274913.jsonl +1 -0
  38. package/conversations/85eb0bf1-8a1a-4e9b-8b98-963f6b274913.meta.json +27 -0
  39. package/conversations/8b1ee339-fcc5-4587-81cd-dc32ea69cfe0.jsonl +1 -0
  40. package/conversations/8b1ee339-fcc5-4587-81cd-dc32ea69cfe0.meta.json +31 -0
  41. package/conversations/9b628dc0-0a71-456b-9ba7-8ca2b0872c63.jsonl +1 -0
  42. package/conversations/9b628dc0-0a71-456b-9ba7-8ca2b0872c63.meta.json +27 -0
  43. package/conversations/ad3ef614-306b-414c-a5c5-ae0f2bd4e3d8.jsonl +1 -0
  44. package/conversations/ad3ef614-306b-414c-a5c5-ae0f2bd4e3d8.meta.json +27 -0
  45. package/conversations/b71f8bc4-3f34-4667-aad1-5ab899339fb0.jsonl +1 -0
  46. package/conversations/b71f8bc4-3f34-4667-aad1-5ab899339fb0.meta.json +27 -0
  47. package/conversations/daf8a65b-83eb-4f7e-8052-714457a8f6b0.jsonl +1 -0
  48. package/conversations/daf8a65b-83eb-4f7e-8052-714457a8f6b0.meta.json +27 -0
  49. package/conversations/f2728631-64b9-4267-a793-2d39e3ce8f5e.jsonl +1 -0
  50. package/conversations/f2728631-64b9-4267-a793-2d39e3ce8f5e.meta.json +27 -0
  51. package/conversations/test-conv-1771128087319.meta.json +27 -0
  52. package/conversations/test-conv-1771128515164.meta.json +27 -0
  53. package/conversations/test-conv-1771128546424.meta.json +27 -0
  54. package/conversations/test-conv-1771128606354.meta.json +27 -0
  55. package/conversations/test-group-1771128087322.meta.json +27 -0
  56. package/conversations/test-group-1771128515165.meta.json +27 -0
  57. package/conversations/test-group-1771128546425.meta.json +27 -0
  58. package/conversations/test-group-1771128606355.meta.json +27 -0
  59. package/demo-two-agents.js +395 -0
  60. package/integrations/express.js +96 -0
  61. package/integrations/openclaw.js +62 -0
  62. package/integrations/webhook.js +111 -0
  63. package/logs/ai2ai-2026-02-15.log +40 -0
  64. package/openclaw-integration.js +540 -0
  65. package/package.json +17 -24
  66. package/package.json.bak +24 -0
  67. package/pending/139dcb76-7778-4130-b448-c7828184a53f.json +28 -0
  68. package/pending/187a69f5-9391-41d0-87d6-34d479a6cc50.json +28 -0
  69. package/pending/2d07e1bb-51f8-4e13-b08b-f1b5b1dc3d1e.json +34 -0
  70. package/pending/2d13bdf4-a818-4629-bfdf-ac29b1a64ba5.json +28 -0
  71. package/pending/3029f00d-97a4-4928-9ff8-3500541c381d.json +31 -0
  72. package/pending/37a3fddb-73e1-4b85-8de5-2def875216bf.json +34 -0
  73. package/pending/4babfd35-aba7-479f-bc0f-f0c83e31d3db.json +34 -0
  74. package/pending/602c0022-993a-4b8a-9ba9-04e56ec59bb5.json +34 -0
  75. package/pending/af925c5f-bed5-4a46-83c3-d16c97d47627.json +28 -0
  76. package/pending/ba1474fe-41b7-412e-b702-0b74307510b9.json +31 -0
  77. package/pending/bcf800f6-c5bb-44a9-8e39-195bd624ff92.json +31 -0
  78. package/pending/c6683665-1321-49ed-8d21-5ae4250848e8.json +31 -0
  79. package/registry.js +406 -0
  80. package/reliability.js +467 -0
  81. package/security.js +386 -0
  82. package/test-v1.js +540 -0
  83. package/test.js +705 -0
  84. package/README.md +0 -184
  85. package/bin/ai2ai.js +0 -87
  86. package/lib/approve.js +0 -137
  87. package/lib/config.js +0 -120
  88. package/lib/connect.js +0 -89
  89. package/lib/contacts.js +0 -62
  90. package/lib/init.js +0 -148
  91. package/lib/pending.js +0 -114
  92. package/lib/protocol.js +0 -161
  93. package/lib/send.js +0 -135
  94. package/lib/start.js +0 -318
  95. package/lib/status.js +0 -70
@@ -0,0 +1,324 @@
1
+ /**
2
+ * AI2AI Conversation Management
3
+ *
4
+ * State machine: proposed → negotiating → confirmed | rejected | expired
5
+ *
6
+ * Features:
7
+ * - Conversation expiry (configurable, default 7 days)
8
+ * - Pending approval cleanup (24hr timeout → auto-reject)
9
+ * - Multi-agent group conversations
10
+ * - Conversation state tracking
11
+ */
12
+
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ const logger = require('./ai2ai-logger');
16
+
17
+ const CONVERSATIONS_DIR = path.join(__dirname, 'conversations');
18
+ const PENDING_DIR = path.join(__dirname, 'pending');
19
+
20
+ // Ensure directories
21
+ [CONVERSATIONS_DIR, PENDING_DIR].forEach(d => {
22
+ if (!fs.existsSync(d)) fs.mkdirSync(d, { recursive: true });
23
+ });
24
+
25
+ // Conversation states
26
+ const STATES = {
27
+ PROPOSED: 'proposed', // Initial request sent/received
28
+ NEGOTIATING: 'negotiating', // Back-and-forth in progress
29
+ CONFIRMED: 'confirmed', // Both parties agreed
30
+ REJECTED: 'rejected', // One party declined
31
+ EXPIRED: 'expired', // Timed out without resolution
32
+ };
33
+
34
+ // Valid state transitions
35
+ const TRANSITIONS = {
36
+ proposed: ['negotiating', 'confirmed', 'rejected', 'expired'],
37
+ negotiating: ['confirmed', 'rejected', 'expired'],
38
+ confirmed: [],
39
+ rejected: [],
40
+ expired: [],
41
+ };
42
+
43
+ // Default config
44
+ const DEFAULT_CONFIG = {
45
+ conversationExpiryDays: 7,
46
+ approvalTimeoutHours: 24,
47
+ };
48
+
49
+ /**
50
+ * Conversation metadata file path
51
+ */
52
+ function metaPath(conversationId) {
53
+ return path.join(CONVERSATIONS_DIR, `${conversationId}.meta.json`);
54
+ }
55
+
56
+ /**
57
+ * Get or create conversation metadata
58
+ */
59
+ function getConversation(conversationId) {
60
+ const mp = metaPath(conversationId);
61
+ if (fs.existsSync(mp)) {
62
+ try {
63
+ return JSON.parse(fs.readFileSync(mp, 'utf-8'));
64
+ } catch { /* fall through */ }
65
+ }
66
+ return null;
67
+ }
68
+
69
+ /**
70
+ * Create a new conversation
71
+ */
72
+ function createConversation(conversationId, { intent, initiator, recipient, participants }) {
73
+ const meta = {
74
+ id: conversationId,
75
+ state: STATES.PROPOSED,
76
+ intent,
77
+ initiator, // { agent, human }
78
+ recipient, // { agent, human } — for 1:1
79
+ participants: participants || [initiator, recipient].filter(Boolean), // for group
80
+ createdAt: new Date().toISOString(),
81
+ updatedAt: new Date().toISOString(),
82
+ expiresAt: new Date(Date.now() + DEFAULT_CONFIG.conversationExpiryDays * 86400000).toISOString(),
83
+ messageCount: 0,
84
+ };
85
+
86
+ fs.writeFileSync(metaPath(conversationId), JSON.stringify(meta, null, 2));
87
+ logger.info('CONV', `Created conversation ${conversationId}`, {
88
+ intent, state: 'proposed', participants: meta.participants,
89
+ });
90
+ return meta;
91
+ }
92
+
93
+ /**
94
+ * Update conversation state with validation
95
+ */
96
+ function transitionState(conversationId, newState) {
97
+ const meta = getConversation(conversationId);
98
+ if (!meta) {
99
+ logger.warn('CONV', `Cannot transition unknown conversation ${conversationId}`);
100
+ return null;
101
+ }
102
+
103
+ const allowed = TRANSITIONS[meta.state];
104
+ if (!allowed || !allowed.includes(newState)) {
105
+ logger.warn('CONV', `Invalid transition: ${meta.state} → ${newState} for ${conversationId}`);
106
+ return null;
107
+ }
108
+
109
+ const oldState = meta.state;
110
+ meta.state = newState;
111
+ meta.updatedAt = new Date().toISOString();
112
+
113
+ fs.writeFileSync(metaPath(conversationId), JSON.stringify(meta, null, 2));
114
+ logger.info('CONV', `Conversation ${conversationId}: ${oldState} → ${newState}`);
115
+ return meta;
116
+ }
117
+
118
+ /**
119
+ * Update conversation metadata (increment message count, etc.)
120
+ */
121
+ function updateConversation(conversationId, updates) {
122
+ const meta = getConversation(conversationId);
123
+ if (!meta) return null;
124
+
125
+ Object.assign(meta, updates, { updatedAt: new Date().toISOString() });
126
+ fs.writeFileSync(metaPath(conversationId), JSON.stringify(meta, null, 2));
127
+ return meta;
128
+ }
129
+
130
+ /**
131
+ * Add a participant to a group conversation
132
+ */
133
+ function addParticipant(conversationId, participant) {
134
+ const meta = getConversation(conversationId);
135
+ if (!meta) return null;
136
+
137
+ if (!meta.participants) meta.participants = [];
138
+
139
+ const exists = meta.participants.find(p => p.agent === participant.agent);
140
+ if (!exists) {
141
+ meta.participants.push(participant);
142
+ meta.updatedAt = new Date().toISOString();
143
+ fs.writeFileSync(metaPath(conversationId), JSON.stringify(meta, null, 2));
144
+ logger.info('CONV', `Added participant ${participant.agent} to ${conversationId}`);
145
+ }
146
+ return meta;
147
+ }
148
+
149
+ /**
150
+ * List all conversations, optionally filtered by state
151
+ */
152
+ function listConversations(stateFilter = null) {
153
+ if (!fs.existsSync(CONVERSATIONS_DIR)) return [];
154
+
155
+ return fs.readdirSync(CONVERSATIONS_DIR)
156
+ .filter(f => f.endsWith('.meta.json'))
157
+ .map(f => {
158
+ try {
159
+ return JSON.parse(fs.readFileSync(path.join(CONVERSATIONS_DIR, f), 'utf-8'));
160
+ } catch { return null; }
161
+ })
162
+ .filter(Boolean)
163
+ .filter(c => !stateFilter || c.state === stateFilter);
164
+ }
165
+
166
+ /**
167
+ * Expire old conversations
168
+ * Returns number of conversations expired
169
+ */
170
+ function expireConversations() {
171
+ const now = new Date();
172
+ let expired = 0;
173
+
174
+ for (const conv of listConversations()) {
175
+ if (conv.state === STATES.CONFIRMED || conv.state === STATES.REJECTED || conv.state === STATES.EXPIRED) {
176
+ continue; // Terminal states
177
+ }
178
+
179
+ const expiresAt = conv.expiresAt ? new Date(conv.expiresAt) : null;
180
+ if (expiresAt && now > expiresAt) {
181
+ transitionState(conv.id, STATES.EXPIRED);
182
+ expired++;
183
+ }
184
+ }
185
+
186
+ if (expired > 0) {
187
+ logger.info('CONV', `Expired ${expired} conversations`);
188
+ }
189
+ return expired;
190
+ }
191
+
192
+ // ─── Pending Approval Management ────────────────────────────────────────────
193
+
194
+ /**
195
+ * Get a pending approval
196
+ */
197
+ function getPendingApproval(approvalId) {
198
+ const pendingPath = path.join(PENDING_DIR, `${approvalId}.json`);
199
+ if (!fs.existsSync(pendingPath)) return null;
200
+ try {
201
+ return JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
202
+ } catch { return null; }
203
+ }
204
+
205
+ /**
206
+ * List all pending approvals
207
+ */
208
+ function listPendingApprovals() {
209
+ if (!fs.existsSync(PENDING_DIR)) return [];
210
+
211
+ return fs.readdirSync(PENDING_DIR)
212
+ .filter(f => f.endsWith('.json'))
213
+ .map(f => {
214
+ try {
215
+ const data = JSON.parse(fs.readFileSync(path.join(PENDING_DIR, f), 'utf-8'));
216
+ data._filename = f;
217
+ return data;
218
+ } catch { return null; }
219
+ })
220
+ .filter(Boolean);
221
+ }
222
+
223
+ /**
224
+ * Resolve a pending approval (approve or reject)
225
+ */
226
+ function resolvePendingApproval(approvalId, approved, humanReply = null) {
227
+ const pendingPath = path.join(PENDING_DIR, `${approvalId}.json`);
228
+ if (!fs.existsSync(pendingPath)) return null;
229
+
230
+ const pending = JSON.parse(fs.readFileSync(pendingPath, 'utf-8'));
231
+ pending.resolved = true;
232
+ pending.approved = approved;
233
+ pending.humanReply = humanReply;
234
+ pending.resolvedAt = new Date().toISOString();
235
+
236
+ // Move to resolved (overwrite in place, or move to archive)
237
+ fs.writeFileSync(pendingPath, JSON.stringify(pending, null, 2));
238
+
239
+ logger.info('APPROVAL', `Approval ${approvalId}: ${approved ? 'APPROVED' : 'REJECTED'}`, {
240
+ humanReply: humanReply?.substring(0, 100),
241
+ });
242
+
243
+ return pending;
244
+ }
245
+
246
+ /**
247
+ * Remove a resolved pending approval
248
+ */
249
+ function removePendingApproval(approvalId) {
250
+ const pendingPath = path.join(PENDING_DIR, `${approvalId}.json`);
251
+ if (fs.existsSync(pendingPath)) {
252
+ fs.unlinkSync(pendingPath);
253
+ }
254
+ }
255
+
256
+ /**
257
+ * Clean up old pending approvals (auto-reject after timeout)
258
+ * @param {number} timeoutHours - Hours before auto-reject (default 24)
259
+ * @returns {{ expired: number, rejected: object[] }}
260
+ */
261
+ function cleanupPendingApprovals(timeoutHours = DEFAULT_CONFIG.approvalTimeoutHours) {
262
+ const cutoff = Date.now() - timeoutHours * 3600000;
263
+ const results = { expired: 0, rejected: [] };
264
+
265
+ for (const pending of listPendingApprovals()) {
266
+ if (pending.resolved) {
267
+ // Clean up old resolved approvals (>7 days)
268
+ const resolvedAt = new Date(pending.resolvedAt || pending.createdAt).getTime();
269
+ if (resolvedAt < Date.now() - 7 * 86400000) {
270
+ removePendingApproval(pending.envelope?.id || pending._filename?.replace('.json', ''));
271
+ }
272
+ continue;
273
+ }
274
+
275
+ const createdAt = new Date(pending.createdAt).getTime();
276
+ if (createdAt < cutoff) {
277
+ // Auto-reject expired approval
278
+ const id = pending.envelope?.id || pending._filename?.replace('.json', '');
279
+ resolvePendingApproval(id, false, 'Auto-rejected: approval timed out');
280
+ results.expired++;
281
+ results.rejected.push(pending);
282
+
283
+ logger.warn('APPROVAL', `Auto-rejected stale approval ${id} (created ${pending.createdAt})`);
284
+ }
285
+ }
286
+
287
+ if (results.expired > 0) {
288
+ logger.info('APPROVAL', `Auto-rejected ${results.expired} stale approvals`);
289
+ }
290
+ return results;
291
+ }
292
+
293
+ /**
294
+ * Run all maintenance tasks
295
+ */
296
+ function runMaintenance() {
297
+ const conversationsExpired = expireConversations();
298
+ const approvalResult = cleanupPendingApprovals();
299
+
300
+ return {
301
+ conversationsExpired,
302
+ approvalsExpired: approvalResult.expired,
303
+ };
304
+ }
305
+
306
+ module.exports = {
307
+ STATES,
308
+ TRANSITIONS,
309
+ getConversation,
310
+ createConversation,
311
+ transitionState,
312
+ updateConversation,
313
+ addParticipant,
314
+ listConversations,
315
+ expireConversations,
316
+ getPendingApproval,
317
+ listPendingApprovals,
318
+ resolvePendingApproval,
319
+ removePendingApproval,
320
+ cleanupPendingApprovals,
321
+ runMaintenance,
322
+ CONVERSATIONS_DIR,
323
+ PENDING_DIR,
324
+ };
@@ -1,17 +1,16 @@
1
1
  /**
2
- * AI2AI Crypto — Ed25519 signing, verification, and key management
3
- * Zero external dependencies uses only Node.js built-in crypto module.
2
+ * AI2AI Crypto — Ed25519 signing and verification
3
+ * Handles key generation, message signing, and signature verification.
4
4
  */
5
5
 
6
- 'use strict';
7
-
8
6
  const crypto = require('crypto');
9
7
  const fs = require('fs');
10
8
  const path = require('path');
11
- const { KEYS_DIR, ensureDirs } = require('./config');
9
+
10
+ const KEYS_DIR = path.join(__dirname, '.keys');
12
11
 
13
12
  /**
14
- * Generate a new Ed25519 key pair
13
+ * Generate a new Ed25519 key pair for this agent
15
14
  */
16
15
  function generateKeyPair() {
17
16
  const { publicKey, privateKey } = crypto.generateKeyPairSync('ed25519', {
@@ -22,57 +21,40 @@ function generateKeyPair() {
22
21
  }
23
22
 
24
23
  /**
25
- * Load keys from ~/.ai2ai/keys/
24
+ * Load or create key pair from disk
26
25
  */
27
- function loadKeys() {
28
- const pubPath = path.join(KEYS_DIR, 'agent.pub');
29
- const privPath = path.join(KEYS_DIR, 'agent.key');
30
-
31
- if (!fs.existsSync(pubPath) || !fs.existsSync(privPath)) {
32
- return null;
26
+ function loadOrCreateKeys() {
27
+ if (!fs.existsSync(KEYS_DIR)) {
28
+ fs.mkdirSync(KEYS_DIR, { recursive: true });
33
29
  }
34
30
 
35
- return {
36
- publicKey: fs.readFileSync(pubPath, 'utf-8'),
37
- privateKey: fs.readFileSync(privPath, 'utf-8'),
38
- };
39
- }
40
-
41
- /**
42
- * Save keys to ~/.ai2ai/keys/
43
- */
44
- function saveKeys(keys) {
45
- ensureDirs();
46
31
  const pubPath = path.join(KEYS_DIR, 'agent.pub');
47
32
  const privPath = path.join(KEYS_DIR, 'agent.key');
48
33
 
34
+ if (fs.existsSync(pubPath) && fs.existsSync(privPath)) {
35
+ return {
36
+ publicKey: fs.readFileSync(pubPath, 'utf-8'),
37
+ privateKey: fs.readFileSync(privPath, 'utf-8'),
38
+ };
39
+ }
40
+
41
+ const keys = generateKeyPair();
49
42
  fs.writeFileSync(pubPath, keys.publicKey, { mode: 0o644 });
50
43
  fs.writeFileSync(privPath, keys.privateKey, { mode: 0o600 });
51
- }
52
-
53
- /**
54
- * Load or create keys
55
- */
56
- function loadOrCreateKeys() {
57
- let keys = loadKeys();
58
- if (!keys) {
59
- keys = generateKeyPair();
60
- saveKeys(keys);
61
- }
62
44
  return keys;
63
45
  }
64
46
 
65
47
  /**
66
- * Get a human-readable fingerprint of a public key
48
+ * Get the public key fingerprint (for human verification)
67
49
  */
68
50
  function getFingerprint(publicKeyPem) {
69
51
  const hash = crypto.createHash('sha256').update(publicKeyPem).digest('hex');
52
+ // Format as groups of 4 for readability
70
53
  return hash.match(/.{1,4}/g).slice(0, 8).join(':');
71
54
  }
72
55
 
73
56
  /**
74
57
  * Sign a message envelope
75
- * Signs the canonical fields to produce a deterministic signature.
76
58
  */
77
59
  function signMessage(envelope, privateKeyPem) {
78
60
  const payload = JSON.stringify({
@@ -115,8 +97,6 @@ function verifyMessage(envelope, signature, publicKeyPem) {
115
97
 
116
98
  module.exports = {
117
99
  generateKeyPair,
118
- loadKeys,
119
- saveKeys,
120
100
  loadOrCreateKeys,
121
101
  getFingerprint,
122
102
  signMessage,