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.
- package/.keys/agent.key +3 -0
- package/.keys/agent.pub +3 -0
- package/.keys/x25519.key.der +0 -0
- package/.keys/x25519.pub.der +0 -0
- package/SKILL.md +39 -0
- package/ai2ai-client.js +351 -0
- package/ai2ai-conversations.js +324 -0
- package/{lib/crypto.js → ai2ai-crypto.js} +19 -39
- package/ai2ai-discovery.js +398 -0
- package/ai2ai-encryption.js +292 -0
- package/ai2ai-handlers.js +392 -0
- package/ai2ai-logger.js +148 -0
- package/ai2ai-queue.js +281 -0
- package/ai2ai-server.js +433 -0
- package/ai2ai-trust.js +137 -0
- package/client.js +434 -0
- package/contacts.example.json +45 -0
- package/contacts.json +57 -0
- package/conversations/06dfc9fc-e0fb-47a6-80ea-6f89b805dcc9.jsonl +1 -0
- package/conversations/06dfc9fc-e0fb-47a6-80ea-6f89b805dcc9.meta.json +31 -0
- package/conversations/132889ee-3c68-4a86-a465-829c467f6782.jsonl +1 -0
- package/conversations/132889ee-3c68-4a86-a465-829c467f6782.meta.json +27 -0
- package/conversations/16c99cf3-7250-4136-8d4a-f5214bcd32ba.jsonl +1 -0
- package/conversations/16c99cf3-7250-4136-8d4a-f5214bcd32ba.meta.json +27 -0
- package/conversations/3f62daf5-49cb-4f9b-9d25-de70625f46e2.jsonl +1 -0
- package/conversations/3f62daf5-49cb-4f9b-9d25-de70625f46e2.meta.json +31 -0
- package/conversations/532b39ab-d513-4e40-98ff-2d3df2d5f256.jsonl +1 -0
- package/conversations/532b39ab-d513-4e40-98ff-2d3df2d5f256.meta.json +27 -0
- package/conversations/5549dc7a-d62e-49f6-93b6-977da7908a11.jsonl +1 -0
- package/conversations/5549dc7a-d62e-49f6-93b6-977da7908a11.meta.json +27 -0
- package/conversations/610ab34a-c7f4-4d24-98b1-75e3f98835d3.jsonl +1 -0
- package/conversations/610ab34a-c7f4-4d24-98b1-75e3f98835d3.meta.json +31 -0
- package/conversations/611610be-28f2-4b31-9afc-b5e278334d8a.jsonl +1 -0
- package/conversations/611610be-28f2-4b31-9afc-b5e278334d8a.meta.json +27 -0
- package/conversations/69ed7660-73bc-4994-9070-db4b264ccd18.jsonl +1 -0
- package/conversations/69ed7660-73bc-4994-9070-db4b264ccd18.meta.json +27 -0
- package/conversations/85eb0bf1-8a1a-4e9b-8b98-963f6b274913.jsonl +1 -0
- package/conversations/85eb0bf1-8a1a-4e9b-8b98-963f6b274913.meta.json +27 -0
- package/conversations/8b1ee339-fcc5-4587-81cd-dc32ea69cfe0.jsonl +1 -0
- package/conversations/8b1ee339-fcc5-4587-81cd-dc32ea69cfe0.meta.json +31 -0
- package/conversations/9b628dc0-0a71-456b-9ba7-8ca2b0872c63.jsonl +1 -0
- package/conversations/9b628dc0-0a71-456b-9ba7-8ca2b0872c63.meta.json +27 -0
- package/conversations/ad3ef614-306b-414c-a5c5-ae0f2bd4e3d8.jsonl +1 -0
- package/conversations/ad3ef614-306b-414c-a5c5-ae0f2bd4e3d8.meta.json +27 -0
- package/conversations/b71f8bc4-3f34-4667-aad1-5ab899339fb0.jsonl +1 -0
- package/conversations/b71f8bc4-3f34-4667-aad1-5ab899339fb0.meta.json +27 -0
- package/conversations/daf8a65b-83eb-4f7e-8052-714457a8f6b0.jsonl +1 -0
- package/conversations/daf8a65b-83eb-4f7e-8052-714457a8f6b0.meta.json +27 -0
- package/conversations/f2728631-64b9-4267-a793-2d39e3ce8f5e.jsonl +1 -0
- package/conversations/f2728631-64b9-4267-a793-2d39e3ce8f5e.meta.json +27 -0
- package/conversations/test-conv-1771128087319.meta.json +27 -0
- package/conversations/test-conv-1771128515164.meta.json +27 -0
- package/conversations/test-conv-1771128546424.meta.json +27 -0
- package/conversations/test-conv-1771128606354.meta.json +27 -0
- package/conversations/test-group-1771128087322.meta.json +27 -0
- package/conversations/test-group-1771128515165.meta.json +27 -0
- package/conversations/test-group-1771128546425.meta.json +27 -0
- package/conversations/test-group-1771128606355.meta.json +27 -0
- package/demo-two-agents.js +395 -0
- package/integrations/express.js +96 -0
- package/integrations/openclaw.js +62 -0
- package/integrations/webhook.js +111 -0
- package/logs/ai2ai-2026-02-15.log +40 -0
- package/openclaw-integration.js +540 -0
- package/package.json +17 -24
- package/package.json.bak +24 -0
- package/pending/139dcb76-7778-4130-b448-c7828184a53f.json +28 -0
- package/pending/187a69f5-9391-41d0-87d6-34d479a6cc50.json +28 -0
- package/pending/2d07e1bb-51f8-4e13-b08b-f1b5b1dc3d1e.json +34 -0
- package/pending/2d13bdf4-a818-4629-bfdf-ac29b1a64ba5.json +28 -0
- package/pending/3029f00d-97a4-4928-9ff8-3500541c381d.json +31 -0
- package/pending/37a3fddb-73e1-4b85-8de5-2def875216bf.json +34 -0
- package/pending/4babfd35-aba7-479f-bc0f-f0c83e31d3db.json +34 -0
- package/pending/602c0022-993a-4b8a-9ba9-04e56ec59bb5.json +34 -0
- package/pending/af925c5f-bed5-4a46-83c3-d16c97d47627.json +28 -0
- package/pending/ba1474fe-41b7-412e-b702-0b74307510b9.json +31 -0
- package/pending/bcf800f6-c5bb-44a9-8e39-195bd624ff92.json +31 -0
- package/pending/c6683665-1321-49ed-8d21-5ae4250848e8.json +31 -0
- package/registry.js +406 -0
- package/reliability.js +467 -0
- package/security.js +386 -0
- package/test-v1.js +540 -0
- package/test.js +705 -0
- package/README.md +0 -184
- package/bin/ai2ai.js +0 -87
- package/lib/approve.js +0 -137
- package/lib/config.js +0 -120
- package/lib/connect.js +0 -89
- package/lib/contacts.js +0 -62
- package/lib/init.js +0 -148
- package/lib/pending.js +0 -114
- package/lib/protocol.js +0 -161
- package/lib/send.js +0 -135
- package/lib/start.js +0 -318
- 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
|
|
3
|
-
*
|
|
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
|
-
|
|
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
|
|
24
|
+
* Load or create key pair from disk
|
|
26
25
|
*/
|
|
27
|
-
function
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
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,
|