a2acalling 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/AGENTS.md +66 -0
- package/CLAUDE.md +52 -0
- package/README.md +307 -0
- package/SKILL.md +122 -0
- package/bin/cli.js +908 -0
- package/docs/protocol.md +241 -0
- package/package.json +44 -0
- package/scripts/install-openclaw.js +291 -0
- package/src/index.js +61 -0
- package/src/lib/call-monitor.js +143 -0
- package/src/lib/client.js +208 -0
- package/src/lib/config.js +173 -0
- package/src/lib/conversations.js +470 -0
- package/src/lib/openclaw-integration.js +329 -0
- package/src/lib/summarizer.js +137 -0
- package/src/lib/tokens.js +448 -0
- package/src/routes/federation.js +463 -0
- package/src/server.js +56 -0
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Federation API Routes
|
|
3
|
+
*
|
|
4
|
+
* Mount at: /api/federation
|
|
5
|
+
*
|
|
6
|
+
* Security notes:
|
|
7
|
+
* - Rate limiting is in-memory (resets on restart) - for production, use Redis
|
|
8
|
+
* - Body size should be limited by Express middleware (e.g., express.json({ limit: '100kb' }))
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { TokenStore } = require('../lib/tokens');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
|
|
14
|
+
// Lazy-load conversation store (optional dependency)
|
|
15
|
+
let ConversationStore = null;
|
|
16
|
+
let conversationStore = null;
|
|
17
|
+
function getConversationStore() {
|
|
18
|
+
if (!ConversationStore) {
|
|
19
|
+
try {
|
|
20
|
+
ConversationStore = require('../lib/conversations').ConversationStore;
|
|
21
|
+
conversationStore = new ConversationStore();
|
|
22
|
+
if (!conversationStore.isAvailable()) {
|
|
23
|
+
conversationStore = null;
|
|
24
|
+
}
|
|
25
|
+
} catch (err) {
|
|
26
|
+
// Conversation storage not available
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return conversationStore;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Lazy-load call monitor
|
|
34
|
+
let CallMonitor = null;
|
|
35
|
+
let callMonitor = null;
|
|
36
|
+
function getCallMonitor(options = {}) {
|
|
37
|
+
if (!CallMonitor) {
|
|
38
|
+
try {
|
|
39
|
+
CallMonitor = require('../lib/call-monitor').CallMonitor;
|
|
40
|
+
} catch (err) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (!callMonitor && options.convStore) {
|
|
45
|
+
callMonitor = new CallMonitor(options);
|
|
46
|
+
callMonitor.start();
|
|
47
|
+
}
|
|
48
|
+
return callMonitor;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Rate limiting state (in-memory - resets on restart)
|
|
52
|
+
// For production: use Redis or persistent store
|
|
53
|
+
const rateLimits = new Map();
|
|
54
|
+
|
|
55
|
+
// Constants
|
|
56
|
+
const MAX_MESSAGE_LENGTH = 10000; // 10KB max message
|
|
57
|
+
const MAX_TIMEOUT_SECONDS = 300; // 5 min max timeout
|
|
58
|
+
const MIN_TIMEOUT_SECONDS = 5; // 5 sec min timeout
|
|
59
|
+
|
|
60
|
+
function checkRateLimit(tokenId, limits = { minute: 10, hour: 100, day: 1000 }) {
|
|
61
|
+
const now = Date.now();
|
|
62
|
+
const minute = Math.floor(now / 60000);
|
|
63
|
+
const hour = Math.floor(now / 3600000);
|
|
64
|
+
const day = Math.floor(now / 86400000);
|
|
65
|
+
|
|
66
|
+
let state = rateLimits.get(tokenId);
|
|
67
|
+
if (!state) {
|
|
68
|
+
state = {
|
|
69
|
+
minute: { count: 0, bucket: minute },
|
|
70
|
+
hour: { count: 0, bucket: hour },
|
|
71
|
+
day: { count: 0, bucket: day }
|
|
72
|
+
};
|
|
73
|
+
rateLimits.set(tokenId, state);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Reset buckets if needed
|
|
77
|
+
if (state.minute.bucket !== minute) state.minute = { count: 0, bucket: minute };
|
|
78
|
+
if (state.hour.bucket !== hour) state.hour = { count: 0, bucket: hour };
|
|
79
|
+
if (state.day.bucket !== day) state.day = { count: 0, bucket: day };
|
|
80
|
+
|
|
81
|
+
// Check limits
|
|
82
|
+
if (state.minute.count >= limits.minute) {
|
|
83
|
+
return { limited: true, error: 'rate_limited', message: 'Too many requests per minute', retryAfter: 60 };
|
|
84
|
+
}
|
|
85
|
+
if (state.hour.count >= limits.hour) {
|
|
86
|
+
return { limited: true, error: 'rate_limited', message: 'Too many requests per hour', retryAfter: 3600 };
|
|
87
|
+
}
|
|
88
|
+
if (state.day.count >= limits.day) {
|
|
89
|
+
return { limited: true, error: 'rate_limited', message: 'Too many requests per day', retryAfter: 86400 };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Increment
|
|
93
|
+
state.minute.count++;
|
|
94
|
+
state.hour.count++;
|
|
95
|
+
state.day.count++;
|
|
96
|
+
|
|
97
|
+
return { limited: false };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Create federation routes
|
|
102
|
+
*
|
|
103
|
+
* @param {object} options
|
|
104
|
+
* @param {TokenStore} options.tokenStore - Token store instance
|
|
105
|
+
* @param {function} options.handleMessage - Async function to handle incoming messages
|
|
106
|
+
* @param {function} options.notifyOwner - Async function to notify owner of calls
|
|
107
|
+
* @param {object} options.rateLimits - Custom rate limits { minute, hour, day }
|
|
108
|
+
* @param {function} options.summarizer - Async function to summarize conversations
|
|
109
|
+
* @param {object} options.ownerContext - Owner context for summaries
|
|
110
|
+
* @param {number} options.idleTimeoutMs - Idle timeout for auto-conclude (default: 60000)
|
|
111
|
+
* @param {number} options.maxDurationMs - Max call duration (default: 300000)
|
|
112
|
+
*/
|
|
113
|
+
function createRoutes(options = {}) {
|
|
114
|
+
const express = require('express');
|
|
115
|
+
const router = express.Router();
|
|
116
|
+
|
|
117
|
+
const tokenStore = options.tokenStore || new TokenStore();
|
|
118
|
+
const handleMessage = options.handleMessage || defaultMessageHandler;
|
|
119
|
+
const notifyOwner = options.notifyOwner || (() => Promise.resolve());
|
|
120
|
+
const limits = options.rateLimits || { minute: 10, hour: 100, day: 1000 };
|
|
121
|
+
|
|
122
|
+
// Initialize conversation store and call monitor
|
|
123
|
+
const convStore = getConversationStore();
|
|
124
|
+
const monitor = getCallMonitor({
|
|
125
|
+
convStore,
|
|
126
|
+
summarizer: options.summarizer,
|
|
127
|
+
notifyOwner,
|
|
128
|
+
ownerContext: options.ownerContext || {},
|
|
129
|
+
idleTimeoutMs: options.idleTimeoutMs || 60000,
|
|
130
|
+
maxDurationMs: options.maxDurationMs || 300000
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* GET /status
|
|
135
|
+
* Check if federation is enabled
|
|
136
|
+
*/
|
|
137
|
+
router.get('/status', (req, res) => {
|
|
138
|
+
res.json({
|
|
139
|
+
federation: true,
|
|
140
|
+
version: require('../../package.json').version,
|
|
141
|
+
capabilities: ['invoke', 'multi-turn'],
|
|
142
|
+
rate_limits: limits
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* GET /ping
|
|
148
|
+
* Simple health check
|
|
149
|
+
*/
|
|
150
|
+
router.get('/ping', (req, res) => {
|
|
151
|
+
res.json({ pong: true, timestamp: new Date().toISOString() });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* POST /invoke
|
|
156
|
+
* Call the agent
|
|
157
|
+
*/
|
|
158
|
+
router.post('/invoke', async (req, res) => {
|
|
159
|
+
// Extract token
|
|
160
|
+
const authHeader = req.headers.authorization;
|
|
161
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
162
|
+
return res.status(401).json({
|
|
163
|
+
success: false,
|
|
164
|
+
error: 'missing_token',
|
|
165
|
+
message: 'Authorization header required'
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const token = authHeader.slice(7);
|
|
170
|
+
|
|
171
|
+
// Validate token
|
|
172
|
+
const validation = tokenStore.validate(token);
|
|
173
|
+
if (!validation.valid) {
|
|
174
|
+
// Use generic error to prevent token enumeration
|
|
175
|
+
// All invalid token states return same response
|
|
176
|
+
return res.status(401).json({
|
|
177
|
+
success: false,
|
|
178
|
+
error: 'unauthorized',
|
|
179
|
+
message: 'Invalid or expired token'
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check rate limit
|
|
184
|
+
const rateCheck = checkRateLimit(validation.id, limits);
|
|
185
|
+
if (rateCheck.limited) {
|
|
186
|
+
res.set('Retry-After', rateCheck.retryAfter);
|
|
187
|
+
return res.status(429).json({
|
|
188
|
+
success: false,
|
|
189
|
+
error: rateCheck.error,
|
|
190
|
+
message: rateCheck.message
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Extract and validate request
|
|
195
|
+
const { message, conversation_id, caller, context, timeout_seconds = 60 } = req.body;
|
|
196
|
+
|
|
197
|
+
if (!message) {
|
|
198
|
+
return res.status(400).json({
|
|
199
|
+
success: false,
|
|
200
|
+
error: 'missing_message',
|
|
201
|
+
message: 'Message is required'
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Validate message length
|
|
206
|
+
if (typeof message !== 'string' || message.length > MAX_MESSAGE_LENGTH) {
|
|
207
|
+
return res.status(400).json({
|
|
208
|
+
success: false,
|
|
209
|
+
error: 'invalid_message',
|
|
210
|
+
message: `Message must be a string under ${MAX_MESSAGE_LENGTH} characters`
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Validate and bound timeout
|
|
215
|
+
const boundedTimeout = Math.max(MIN_TIMEOUT_SECONDS, Math.min(MAX_TIMEOUT_SECONDS, Number(timeout_seconds) || 60));
|
|
216
|
+
|
|
217
|
+
// Sanitize caller data (only allow expected fields)
|
|
218
|
+
const sanitizedCaller = caller ? {
|
|
219
|
+
name: String(caller.name || '').slice(0, 100),
|
|
220
|
+
instance: String(caller.instance || '').slice(0, 200),
|
|
221
|
+
context: String(caller.context || '').slice(0, 500)
|
|
222
|
+
} : {};
|
|
223
|
+
|
|
224
|
+
// Build federation context with secure conversation ID
|
|
225
|
+
const isNewConversation = !conversation_id;
|
|
226
|
+
const federationContext = {
|
|
227
|
+
mode: 'federation',
|
|
228
|
+
token_id: validation.id,
|
|
229
|
+
token_name: validation.name,
|
|
230
|
+
tier: validation.tier,
|
|
231
|
+
allowed_topics: validation.allowed_topics,
|
|
232
|
+
disclosure: validation.disclosure,
|
|
233
|
+
caller: sanitizedCaller,
|
|
234
|
+
conversation_id: conversation_id || `conv_${Date.now()}_${crypto.randomBytes(6).toString('hex')}`
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// Track conversation if store available
|
|
238
|
+
if (convStore) {
|
|
239
|
+
try {
|
|
240
|
+
convStore.startConversation({
|
|
241
|
+
id: federationContext.conversation_id,
|
|
242
|
+
contactId: validation.id,
|
|
243
|
+
contactName: sanitizedCaller.name || validation.name,
|
|
244
|
+
tokenId: validation.id,
|
|
245
|
+
direction: 'inbound'
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Track activity for auto-conclude
|
|
249
|
+
if (monitor) {
|
|
250
|
+
monitor.trackActivity(federationContext.conversation_id, sanitizedCaller);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Store incoming message
|
|
254
|
+
convStore.addMessage(federationContext.conversation_id, {
|
|
255
|
+
direction: 'inbound',
|
|
256
|
+
role: 'user',
|
|
257
|
+
content: message
|
|
258
|
+
});
|
|
259
|
+
} catch (err) {
|
|
260
|
+
console.error('[a2a] Conversation tracking error:', err.message);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
// Handle the message
|
|
266
|
+
const response = await handleMessage(message, federationContext, { timeout: boundedTimeout * 1000 });
|
|
267
|
+
|
|
268
|
+
// Store outgoing response
|
|
269
|
+
if (convStore) {
|
|
270
|
+
try {
|
|
271
|
+
convStore.addMessage(federationContext.conversation_id, {
|
|
272
|
+
direction: 'outbound',
|
|
273
|
+
role: 'assistant',
|
|
274
|
+
content: response.text
|
|
275
|
+
});
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.error('[a2a] Message storage error:', err.message);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Notify owner if configured
|
|
282
|
+
if (validation.notify !== 'none') {
|
|
283
|
+
notifyOwner({
|
|
284
|
+
level: validation.notify,
|
|
285
|
+
token: validation,
|
|
286
|
+
caller,
|
|
287
|
+
context,
|
|
288
|
+
message,
|
|
289
|
+
response: response.text,
|
|
290
|
+
conversation_id: federationContext.conversation_id
|
|
291
|
+
}).catch(err => {
|
|
292
|
+
console.error('[a2a] Failed to notify owner:', err.message);
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
res.json({
|
|
297
|
+
success: true,
|
|
298
|
+
conversation_id: federationContext.conversation_id,
|
|
299
|
+
response: response.text,
|
|
300
|
+
can_continue: response.canContinue !== false,
|
|
301
|
+
tokens_remaining: validation.calls_remaining
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
} catch (err) {
|
|
305
|
+
console.error('[a2a] Message handling error:', err);
|
|
306
|
+
res.status(500).json({
|
|
307
|
+
success: false,
|
|
308
|
+
error: 'internal_error',
|
|
309
|
+
message: 'Failed to process message'
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* POST /end
|
|
316
|
+
* End a conversation and trigger summary generation
|
|
317
|
+
*/
|
|
318
|
+
router.post('/end', async (req, res) => {
|
|
319
|
+
// Extract token
|
|
320
|
+
const authHeader = req.headers.authorization;
|
|
321
|
+
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
322
|
+
return res.status(401).json({
|
|
323
|
+
success: false,
|
|
324
|
+
error: 'unauthorized',
|
|
325
|
+
message: 'Authorization header required'
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const token = authHeader.slice(7);
|
|
330
|
+
const validation = tokenStore.validate(token);
|
|
331
|
+
if (!validation.valid) {
|
|
332
|
+
return res.status(401).json({
|
|
333
|
+
success: false,
|
|
334
|
+
error: 'unauthorized',
|
|
335
|
+
message: 'Invalid or expired token'
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const { conversation_id } = req.body;
|
|
340
|
+
if (!conversation_id) {
|
|
341
|
+
return res.status(400).json({
|
|
342
|
+
success: false,
|
|
343
|
+
error: 'missing_conversation_id',
|
|
344
|
+
message: 'conversation_id is required'
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const convStore = getConversationStore();
|
|
349
|
+
if (!convStore) {
|
|
350
|
+
return res.json({ success: true, message: 'Conversation storage not enabled' });
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
try {
|
|
354
|
+
// Conclude with summarizer if available
|
|
355
|
+
const summarizer = options.summarizer || null;
|
|
356
|
+
const ownerContext = options.ownerContext || {};
|
|
357
|
+
|
|
358
|
+
const result = await convStore.concludeConversation(conversation_id, {
|
|
359
|
+
summarizer,
|
|
360
|
+
ownerContext
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Notify owner of conversation conclusion
|
|
364
|
+
if (validation.notify !== 'none' && result.success) {
|
|
365
|
+
const conv = convStore.getConversationContext(conversation_id);
|
|
366
|
+
notifyOwner({
|
|
367
|
+
level: validation.notify,
|
|
368
|
+
type: 'conversation_concluded',
|
|
369
|
+
token: validation,
|
|
370
|
+
conversation: conv
|
|
371
|
+
}).catch(err => {
|
|
372
|
+
console.error('[a2a] Failed to notify owner:', err.message);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
res.json({
|
|
377
|
+
success: true,
|
|
378
|
+
conversation_id,
|
|
379
|
+
status: 'concluded',
|
|
380
|
+
summary: result.summary
|
|
381
|
+
});
|
|
382
|
+
} catch (err) {
|
|
383
|
+
console.error('[a2a] End conversation error:', err);
|
|
384
|
+
res.status(500).json({
|
|
385
|
+
success: false,
|
|
386
|
+
error: 'internal_error',
|
|
387
|
+
message: 'Failed to end conversation'
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* GET /conversations
|
|
394
|
+
* List conversations (requires auth)
|
|
395
|
+
* This is for the agent owner, not federated callers
|
|
396
|
+
*/
|
|
397
|
+
router.get('/conversations', (req, res) => {
|
|
398
|
+
// This endpoint should be protected by local auth, not federation tokens
|
|
399
|
+
// For now, require an admin token or local access
|
|
400
|
+
const adminToken = req.headers['x-admin-token'];
|
|
401
|
+
if (adminToken !== process.env.A2A_ADMIN_TOKEN && req.ip !== '127.0.0.1') {
|
|
402
|
+
return res.status(401).json({ error: 'unauthorized' });
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const convStore = getConversationStore();
|
|
406
|
+
if (!convStore) {
|
|
407
|
+
return res.json({ conversations: [], message: 'Conversation storage not enabled' });
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const { contact_id, status, limit = 20 } = req.query;
|
|
411
|
+
|
|
412
|
+
const conversations = convStore.listConversations({
|
|
413
|
+
contactId: contact_id,
|
|
414
|
+
status,
|
|
415
|
+
limit: parseInt(limit),
|
|
416
|
+
includeMessages: false
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
res.json({ conversations });
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* GET /conversations/:id
|
|
424
|
+
* Get conversation details with context
|
|
425
|
+
*/
|
|
426
|
+
router.get('/conversations/:id', (req, res) => {
|
|
427
|
+
const adminToken = req.headers['x-admin-token'];
|
|
428
|
+
if (adminToken !== process.env.A2A_ADMIN_TOKEN && req.ip !== '127.0.0.1') {
|
|
429
|
+
return res.status(401).json({ error: 'unauthorized' });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const convStore = getConversationStore();
|
|
433
|
+
if (!convStore) {
|
|
434
|
+
return res.status(404).json({ error: 'conversation_storage_disabled' });
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const { recent_messages = 10 } = req.query;
|
|
438
|
+
const context = convStore.getConversationContext(
|
|
439
|
+
req.params.id,
|
|
440
|
+
parseInt(recent_messages)
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
if (!context) {
|
|
444
|
+
return res.status(404).json({ error: 'conversation_not_found' });
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
res.json(context);
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
return router;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Default message handler (placeholder)
|
|
455
|
+
*/
|
|
456
|
+
async function defaultMessageHandler(message, context, options) {
|
|
457
|
+
return {
|
|
458
|
+
text: `[A2A Federation Active] Received message from ${context.caller?.name || 'unknown'}. Agent integration pending.`,
|
|
459
|
+
canContinue: true
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
module.exports = { createRoutes, checkRateLimit };
|
package/src/server.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* A2A Federation Server
|
|
4
|
+
*
|
|
5
|
+
* Standalone server for testing or running alongside OpenClaw.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* node src/server.js [--port 3001]
|
|
9
|
+
* PORT=3001 node src/server.js
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const express = require('express');
|
|
13
|
+
const { createRoutes } = require('./routes/federation');
|
|
14
|
+
const { TokenStore } = require('./lib/tokens');
|
|
15
|
+
|
|
16
|
+
const port = process.env.PORT || parseInt(process.argv[2]) || 3001;
|
|
17
|
+
|
|
18
|
+
const app = express();
|
|
19
|
+
app.use(express.json());
|
|
20
|
+
|
|
21
|
+
// Initialize token store
|
|
22
|
+
const tokenStore = new TokenStore();
|
|
23
|
+
|
|
24
|
+
// Mount federation routes
|
|
25
|
+
app.use('/api/federation', createRoutes({
|
|
26
|
+
tokenStore,
|
|
27
|
+
|
|
28
|
+
// Default message handler - in production, this connects to the agent
|
|
29
|
+
async handleMessage(message, context, options) {
|
|
30
|
+
console.log(`[a2a] Received message from ${context.caller?.name || 'unknown'}: ${message}`);
|
|
31
|
+
return {
|
|
32
|
+
text: `[A2A Federation Active] Received: "${message}". Full agent integration pending.`,
|
|
33
|
+
canContinue: true
|
|
34
|
+
};
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
// Default owner notification - in production, this sends to chat
|
|
38
|
+
async notifyOwner({ level, token, caller, message, response }) {
|
|
39
|
+
console.log(`[a2a] Notification (${level}): ${caller?.name || 'unknown'} called via token "${token.name}"`);
|
|
40
|
+
console.log(`[a2a] Message: ${message}`);
|
|
41
|
+
console.log(`[a2a] Response: ${response}`);
|
|
42
|
+
}
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Health check at root
|
|
46
|
+
app.get('/', (req, res) => {
|
|
47
|
+
res.json({ service: 'a2a-federation', status: 'ok' });
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
app.listen(port, () => {
|
|
51
|
+
console.log(`[a2a] Federation server listening on port ${port}`);
|
|
52
|
+
console.log(`[a2a] Endpoints:`);
|
|
53
|
+
console.log(`[a2a] GET /api/federation/status`);
|
|
54
|
+
console.log(`[a2a] GET /api/federation/ping`);
|
|
55
|
+
console.log(`[a2a] POST /api/federation/invoke`);
|
|
56
|
+
});
|