@axiom-lattice/gateway 2.1.88 → 2.1.90
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/.turbo/turbo-build.log +14 -12
- package/AGENTS.md +62 -0
- package/CHANGELOG.md +22 -0
- package/dist/a2a-ERG5RMUW.mjs +567 -0
- package/dist/a2a-ERG5RMUW.mjs.map +1 -0
- package/dist/index.d.mts +62 -0
- package/dist/index.d.ts +62 -0
- package/dist/index.js +879 -29
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +294 -16
- package/dist/index.mjs.map +1 -1
- package/jest.config.js +1 -1
- package/package.json +6 -6
- package/src/__tests__/a2a.test.ts +346 -0
- package/src/index.ts +19 -0
- package/src/router/MessageRouter.ts +169 -17
- package/src/routes/a2a-bridge.ts +266 -0
- package/src/routes/a2a.ts +779 -0
- package/src/routes/index.ts +5 -0
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A2A Bridge — WebSocket-based reverse proxy for agents behind NAT.
|
|
3
|
+
*
|
|
4
|
+
* Agents open an outbound WebSocket to the gateway. The gateway stores
|
|
5
|
+
* the connection and exposes an HTTP proxy endpoint per agent. When the
|
|
6
|
+
* orchestrator calls the agent, the gateway proxies the HTTP request
|
|
7
|
+
* over the persistent WebSocket instead of trying to reach a private IP.
|
|
8
|
+
*
|
|
9
|
+
* Agent flow:
|
|
10
|
+
* 1. Connect ws://gateway/api/a2a/bridge
|
|
11
|
+
* 2. Send { type: "agent:register", agentId: "...", agentCard: {...} }
|
|
12
|
+
* 3. Wait for { type: "http:request", requestId, method, path, headers, body }
|
|
13
|
+
* 4. Process locally, respond with { type: "http:response", requestId, status, headers, body }
|
|
14
|
+
*
|
|
15
|
+
* Gateway flow:
|
|
16
|
+
* 1. Accept WebSocket, store connection by agentId
|
|
17
|
+
* 2. Expose GET/POST /api/a2a/bridge/proxy/{agentId}/*
|
|
18
|
+
* 3. When HTTP request arrives, push over WebSocket, await response, relay
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { FastifyInstance, FastifyRequest, FastifyReply } from 'fastify';
|
|
22
|
+
import type { WebSocket } from 'ws';
|
|
23
|
+
import { v4 } from 'uuid';
|
|
24
|
+
|
|
25
|
+
// ─── Logging ────────────────────────────────────────────────────────────────
|
|
26
|
+
|
|
27
|
+
const log = {
|
|
28
|
+
info(msg: string, ctx?: Record<string, unknown>) {
|
|
29
|
+
console.log(JSON.stringify({ level: 'info', msg, ...ctx }));
|
|
30
|
+
},
|
|
31
|
+
warn(msg: string, ctx?: Record<string, unknown>) {
|
|
32
|
+
console.warn(JSON.stringify({ level: 'warn', msg, ...ctx }));
|
|
33
|
+
},
|
|
34
|
+
error(msg: string, ctx?: Record<string, unknown>) {
|
|
35
|
+
console.error(JSON.stringify({ level: 'error', msg, ...ctx }));
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ─── Types ──────────────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
interface PendingRequest {
|
|
42
|
+
resolve: (response: BridgeHttpResponse) => void;
|
|
43
|
+
reject: (error: Error) => void;
|
|
44
|
+
timer: ReturnType<typeof setTimeout>;
|
|
45
|
+
/** The agent owning this pending request — used for cleanup on disconnect */
|
|
46
|
+
agentId: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface BridgeHttpResponse {
|
|
50
|
+
status: number;
|
|
51
|
+
headers: Record<string, string>;
|
|
52
|
+
body: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface AgentConnection {
|
|
56
|
+
ws: WebSocket;
|
|
57
|
+
agentId: string;
|
|
58
|
+
agentCard: Record<string, unknown>;
|
|
59
|
+
connectedAt: number;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Connection Registry ────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
const connections = new Map<string, AgentConnection>();
|
|
65
|
+
const pending = new Map<string, PendingRequest>();
|
|
66
|
+
|
|
67
|
+
function getAgentIdFromPath(path: string): string | null {
|
|
68
|
+
// Path: /api/a2a/bridge/proxy/{agentId}/...
|
|
69
|
+
const match = path.match(/^\/api\/a2a\/bridge\/proxy\/([^/]+)/);
|
|
70
|
+
return match ? match[1] : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ─── WebSocket Handler ──────────────────────────────────────────────────────
|
|
74
|
+
|
|
75
|
+
function handleBridgeConnection(socket: WebSocket, _request: FastifyRequest): void {
|
|
76
|
+
let agentId: string | null = null;
|
|
77
|
+
|
|
78
|
+
log.info('Bridge WebSocket connected');
|
|
79
|
+
|
|
80
|
+
socket.on('message', (raw) => {
|
|
81
|
+
let msg: Record<string, unknown>;
|
|
82
|
+
try {
|
|
83
|
+
msg = JSON.parse(raw.toString());
|
|
84
|
+
} catch {
|
|
85
|
+
log.warn('Bridge invalid message');
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
switch (msg.type) {
|
|
90
|
+
case 'agent:register': {
|
|
91
|
+
agentId = msg.agentId as string;
|
|
92
|
+
if (!agentId) {
|
|
93
|
+
socket.send(JSON.stringify({ type: 'error', message: 'agentId required' }));
|
|
94
|
+
socket.close();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
connections.set(agentId, {
|
|
98
|
+
ws: socket,
|
|
99
|
+
agentId,
|
|
100
|
+
agentCard: msg.agentCard as Record<string, unknown> ?? {},
|
|
101
|
+
connectedAt: Date.now(),
|
|
102
|
+
});
|
|
103
|
+
log.info('Agent registered via bridge', { agentId });
|
|
104
|
+
socket.send(JSON.stringify({ type: 'agent:registered', agentId }));
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'http:response': {
|
|
109
|
+
const requestId = msg.requestId as string;
|
|
110
|
+
const p = pending.get(requestId);
|
|
111
|
+
if (p) {
|
|
112
|
+
clearTimeout(p.timer);
|
|
113
|
+
pending.delete(requestId);
|
|
114
|
+
p.resolve({
|
|
115
|
+
status: (msg.status as number) ?? 200,
|
|
116
|
+
headers: (msg.headers as Record<string, string>) ?? {},
|
|
117
|
+
body: (msg.body as string) ?? '',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
case 'http:error': {
|
|
124
|
+
const requestId = msg.requestId as string;
|
|
125
|
+
const p = pending.get(requestId);
|
|
126
|
+
if (p) {
|
|
127
|
+
clearTimeout(p.timer);
|
|
128
|
+
pending.delete(requestId);
|
|
129
|
+
p.reject(new Error((msg.message as string) ?? 'Agent error'));
|
|
130
|
+
}
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
socket.on('close', () => {
|
|
137
|
+
if (agentId) {
|
|
138
|
+
connections.delete(agentId);
|
|
139
|
+
log.info('Bridge WebSocket disconnected', { agentId });
|
|
140
|
+
// Reject only this agent's pending requests
|
|
141
|
+
for (const [reqId, p] of pending) {
|
|
142
|
+
if (p.agentId === agentId) {
|
|
143
|
+
clearTimeout(p.timer);
|
|
144
|
+
p.reject(new Error('Agent disconnected'));
|
|
145
|
+
pending.delete(reqId);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
socket.on('error', (err) => {
|
|
152
|
+
log.error('Bridge WebSocket error', { agentId, error: err.message });
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ─── HTTP Proxy Handler ─────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
async function handleBridgeProxy(
|
|
159
|
+
request: FastifyRequest,
|
|
160
|
+
reply: FastifyReply,
|
|
161
|
+
): Promise<void> {
|
|
162
|
+
const agentId = getAgentIdFromPath(request.url);
|
|
163
|
+
if (!agentId) {
|
|
164
|
+
reply.status(400).send({ error: 'Agent ID not found in path' });
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const conn = connections.get(agentId);
|
|
169
|
+
if (!conn) {
|
|
170
|
+
reply.status(503).send({
|
|
171
|
+
error: 'Agent not connected',
|
|
172
|
+
message: `Agent "${agentId}" is not connected via WebSocket bridge`,
|
|
173
|
+
});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const requestId = v4();
|
|
178
|
+
|
|
179
|
+
// Build sub-path (strip the proxy prefix)
|
|
180
|
+
const subPath = request.url.replace(`/api/a2a/bridge/proxy/${agentId}`, '') || '/';
|
|
181
|
+
|
|
182
|
+
// Read body
|
|
183
|
+
let body: string | undefined;
|
|
184
|
+
if (request.method === 'POST' || request.method === 'PUT') {
|
|
185
|
+
body = typeof request.body === 'string'
|
|
186
|
+
? request.body
|
|
187
|
+
: JSON.stringify(request.body ?? {});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const bridgeRequest = {
|
|
191
|
+
type: 'http:request',
|
|
192
|
+
requestId,
|
|
193
|
+
method: request.method,
|
|
194
|
+
path: subPath,
|
|
195
|
+
headers: {
|
|
196
|
+
'content-type': request.headers['content-type'] ?? 'application/json',
|
|
197
|
+
'accept': request.headers['accept'] ?? '*/*',
|
|
198
|
+
},
|
|
199
|
+
body: body ?? '',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
const timeout = 300_000; // 5 min
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const response = await new Promise<BridgeHttpResponse>((resolve, reject) => {
|
|
206
|
+
const timer = setTimeout(() => {
|
|
207
|
+
pending.delete(requestId);
|
|
208
|
+
reject(new Error(`Bridge request timeout after ${timeout}ms`));
|
|
209
|
+
}, timeout);
|
|
210
|
+
|
|
211
|
+
pending.set(requestId, { resolve, reject, timer, agentId });
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
if (conn.ws.readyState !== conn.ws.OPEN) {
|
|
215
|
+
pending.delete(requestId);
|
|
216
|
+
reject(new Error('WebSocket not open'));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
conn.ws.send(JSON.stringify(bridgeRequest));
|
|
220
|
+
} catch (err) {
|
|
221
|
+
clearTimeout(timer);
|
|
222
|
+
pending.delete(requestId);
|
|
223
|
+
reject(err as Error);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// Relay response headers
|
|
228
|
+
for (const [key, value] of Object.entries(response.headers)) {
|
|
229
|
+
if (!['content-length', 'transfer-encoding', 'connection'].includes(key.toLowerCase())) {
|
|
230
|
+
reply.header(key, value);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
reply.status(response.status);
|
|
235
|
+
|
|
236
|
+
// Try to send as JSON if possible
|
|
237
|
+
try {
|
|
238
|
+
const parsed = JSON.parse(response.body);
|
|
239
|
+
reply.send(parsed);
|
|
240
|
+
} catch {
|
|
241
|
+
reply.send(response.body);
|
|
242
|
+
}
|
|
243
|
+
} catch (err) {
|
|
244
|
+
log.error('Bridge proxy error', { agentId, error: (err as Error).message });
|
|
245
|
+
reply.status(502).send({
|
|
246
|
+
error: 'Bridge proxy error',
|
|
247
|
+
message: (err as Error).message,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ─── Route Registration ─────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export function registerA2ABridgeRoutes(app: FastifyInstance): void {
|
|
255
|
+
// WebSocket endpoint for agents to connect
|
|
256
|
+
app.get('/api/a2a/bridge', { websocket: true }, (socket, req) => {
|
|
257
|
+
handleBridgeConnection(socket, req);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
// HTTP proxy endpoint — all paths under /api/a2a/bridge/proxy/{agentId}/
|
|
261
|
+
app.route({
|
|
262
|
+
method: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
|
|
263
|
+
url: '/api/a2a/bridge/proxy/*',
|
|
264
|
+
handler: handleBridgeProxy,
|
|
265
|
+
});
|
|
266
|
+
}
|