@axiom-lattice/gateway 3.0.2 → 3.0.4

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 (155) hide show
  1. package/dist/index.js +37 -9
  2. package/dist/index.js.map +1 -1
  3. package/dist/index.mjs +37 -9
  4. package/dist/index.mjs.map +1 -1
  5. package/package.json +9 -6
  6. package/.eslintrc.json +0 -22
  7. package/.turbo/turbo-build.log +0 -46
  8. package/AGENTS.md +0 -112
  9. package/CHANGELOG.md +0 -1190
  10. package/LOGGER_CONFIG.md +0 -135
  11. package/RESUME_STREAM_CONTENT_BASED.md +0 -326
  12. package/RESUME_STREAM_README.md +0 -388
  13. package/jest.config.js +0 -72
  14. package/public/sdk/README.md +0 -695
  15. package/public/sdk/dashboard-engine-skill.md +0 -1122
  16. package/public/sdk/dashboard-single-file-spec.md +0 -357
  17. package/public/sdk/data-query-sdk-skill.md +0 -307
  18. package/public/sdk/data-query-sdk.d.ts +0 -252
  19. package/public/sdk/data-query-sdk.js +0 -970
  20. package/public/sdk/occupancy-dashboard.html +0 -363
  21. package/public/sdk/test-dashboard.html +0 -690
  22. package/scripts/seed-lark-installation.ts +0 -54
  23. package/src/__tests__/__mocks__/e2b.ts +0 -1
  24. package/src/__tests__/a2a.test.ts +0 -428
  25. package/src/__tests__/channel-installations.test.ts +0 -199
  26. package/src/__tests__/data-query.test.ts +0 -77
  27. package/src/__tests__/sandbox-provider-registration.test.ts +0 -74
  28. package/src/__tests__/setup.ts +0 -5
  29. package/src/__tests__/workspace.test.ts +0 -400
  30. package/src/channels/__tests__/routes.test.ts +0 -50
  31. package/src/channels/lark/LarkChannelAdapter.ts +0 -217
  32. package/src/channels/lark/README.md +0 -187
  33. package/src/channels/lark/__tests__/controller.test.ts +0 -136
  34. package/src/channels/lark/__tests__/mapping-service.test.ts +0 -118
  35. package/src/channels/lark/__tests__/parser.test.ts +0 -72
  36. package/src/channels/lark/__tests__/sender.test.ts +0 -37
  37. package/src/channels/lark/__tests__/verification.test.ts +0 -157
  38. package/src/channels/lark/controller.ts +0 -48
  39. package/src/channels/lark/mapping-service.ts +0 -138
  40. package/src/channels/lark/parser.ts +0 -68
  41. package/src/channels/lark/routes.ts +0 -22
  42. package/src/channels/lark/sender.ts +0 -58
  43. package/src/channels/lark/types.ts +0 -20
  44. package/src/channels/lark/verification.ts +0 -32
  45. package/src/channels/registry.ts +0 -20
  46. package/src/channels/routes.ts +0 -30
  47. package/src/channels/wechat/WechatChannelAdapter.ts +0 -294
  48. package/src/channels/wechat/__tests__/context-store.test.ts +0 -148
  49. package/src/channels/wechat/context-store.ts +0 -54
  50. package/src/channels/wechat/controller.ts +0 -59
  51. package/src/channels/wechat/routes.ts +0 -7
  52. package/src/channels/wechat/types.ts +0 -62
  53. package/src/channels/wechat/wechat-client.ts +0 -162
  54. package/src/config/local-a2a-templates.ts +0 -59
  55. package/src/config.ts +0 -124
  56. package/src/controllers/__tests__/assistant-local-a2a.test.ts +0 -147
  57. package/src/controllers/__tests__/local-a2a-templates.test.ts +0 -112
  58. package/src/controllers/__tests__/mime.test.ts +0 -74
  59. package/src/controllers/__tests__/resources.test.ts +0 -122
  60. package/src/controllers/__tests__/run.test.ts +0 -115
  61. package/src/controllers/__tests__/tasks.test.ts +0 -215
  62. package/src/controllers/__tests__/workflow-tracking.test.ts +0 -113
  63. package/src/controllers/agent_task.ts +0 -72
  64. package/src/controllers/assistant.ts +0 -490
  65. package/src/controllers/auth.ts +0 -524
  66. package/src/controllers/channel-bindings.ts +0 -135
  67. package/src/controllers/channel-installations.ts +0 -409
  68. package/src/controllers/collections.ts +0 -176
  69. package/src/controllers/config.ts +0 -126
  70. package/src/controllers/connections.ts +0 -181
  71. package/src/controllers/data-query.ts +0 -236
  72. package/src/controllers/database-configs.ts +0 -440
  73. package/src/controllers/embeddings.ts +0 -22
  74. package/src/controllers/entries.ts +0 -167
  75. package/src/controllers/eval.ts +0 -513
  76. package/src/controllers/export-import.ts +0 -166
  77. package/src/controllers/health.ts +0 -36
  78. package/src/controllers/local-a2a-templates.ts +0 -18
  79. package/src/controllers/local-a2a.ts +0 -149
  80. package/src/controllers/mcp-configs.ts +0 -673
  81. package/src/controllers/memory.ts +0 -257
  82. package/src/controllers/menu-items.ts +0 -114
  83. package/src/controllers/metrics-configs.ts +0 -1061
  84. package/src/controllers/middleware-types.ts +0 -15
  85. package/src/controllers/models.ts +0 -199
  86. package/src/controllers/personal-assistant.ts +0 -191
  87. package/src/controllers/resources.ts +0 -477
  88. package/src/controllers/run.ts +0 -338
  89. package/src/controllers/sandbox.ts +0 -255
  90. package/src/controllers/schedules.ts +0 -336
  91. package/src/controllers/skills.ts +0 -256
  92. package/src/controllers/tasks.ts +0 -213
  93. package/src/controllers/tenants.ts +0 -121
  94. package/src/controllers/thread_status.ts +0 -66
  95. package/src/controllers/threads.ts +0 -260
  96. package/src/controllers/tools.ts +0 -139
  97. package/src/controllers/users.ts +0 -135
  98. package/src/controllers/workflow-tracking.ts +0 -693
  99. package/src/controllers/workspace.ts +0 -879
  100. package/src/export_registrations/a2a-api-key.registration.ts +0 -89
  101. package/src/export_registrations/agent.registration.ts +0 -80
  102. package/src/export_registrations/binding.registration.ts +0 -106
  103. package/src/export_registrations/channel-installation.registration.ts +0 -88
  104. package/src/export_registrations/collection.registration.ts +0 -85
  105. package/src/export_registrations/connection.registration.ts +0 -97
  106. package/src/export_registrations/database-config.registration.ts +0 -94
  107. package/src/export_registrations/eval.registration.ts +0 -334
  108. package/src/export_registrations/index.ts +0 -31
  109. package/src/export_registrations/mcp-config.registration.ts +0 -97
  110. package/src/export_registrations/menu.registration.ts +0 -91
  111. package/src/export_registrations/metrics-config.registration.ts +0 -94
  112. package/src/export_registrations/skill.registration.ts +0 -88
  113. package/src/export_registrations/task.registration.ts +0 -99
  114. package/src/index.ts +0 -497
  115. package/src/logger/Logger.ts +0 -186
  116. package/src/router/MessageContext.ts +0 -14
  117. package/src/router/MessageRouter.ts +0 -490
  118. package/src/router/middlewares/auditLogger.ts +0 -34
  119. package/src/router/middlewares/deduplication.ts +0 -38
  120. package/src/router/middlewares/index.ts +0 -3
  121. package/src/router/middlewares/rateLimit.ts +0 -46
  122. package/src/routes/__tests__/a2a-test-connection.test.ts +0 -177
  123. package/src/routes/a2a-bridge.ts +0 -266
  124. package/src/routes/a2a-test-connection.ts +0 -126
  125. package/src/routes/a2a.ts +0 -851
  126. package/src/routes/channel-bindings.ts +0 -11
  127. package/src/routes/channel-installations.ts +0 -33
  128. package/src/routes/index.ts +0 -640
  129. package/src/routes/menu-items.ts +0 -10
  130. package/src/routes/resource-routes.ts +0 -35
  131. package/src/routes/stt.ts +0 -102
  132. package/src/schemas/data-query.ts +0 -69
  133. package/src/schemas/index.ts +0 -852
  134. package/src/services/ExportImportService.ts +0 -296
  135. package/src/services/__tests__/ExportImportService.test.ts +0 -130
  136. package/src/services/__tests__/eval-runner.test.ts +0 -131
  137. package/src/services/__tests__/local-a2a.test.ts +0 -124
  138. package/src/services/agent_task_consumer.ts +0 -348
  139. package/src/services/collection_service.ts +0 -258
  140. package/src/services/eval-runner.ts +0 -283
  141. package/src/services/local-a2a.ts +0 -354
  142. package/src/services/queue_service.ts +0 -104
  143. package/src/services/sandbox_service.ts +0 -223
  144. package/src/services/skill_service.ts +0 -97
  145. package/src/services/supabase.ts +0 -60
  146. package/src/swagger.ts +0 -77
  147. package/src/types/index.ts +0 -118
  148. package/src/types/koa.d.ts +0 -7
  149. package/src/utils/__tests__/local-a2a-runtime.test.ts +0 -77
  150. package/src/utils/__tests__/path-containment.test.ts +0 -35
  151. package/src/utils/a2a-url.ts +0 -20
  152. package/src/utils/local-a2a-runtime.ts +0 -107
  153. package/src/utils/mime.ts +0 -44
  154. package/src/utils/path-containment.ts +0 -38
  155. package/tsconfig.json +0 -26
@@ -1,34 +0,0 @@
1
- import type { MessageMiddleware } from "../MessageContext";
2
- import { Logger } from "../../logger/Logger";
3
-
4
- const logger = new Logger({ serviceName: "lattice/gateway/audit" });
5
-
6
- export function createAuditLoggerMiddleware(): MessageMiddleware {
7
- return async (ctx, next) => {
8
- const start = Date.now();
9
- try {
10
- await next();
11
- logger.info("message routed", {
12
- event: "message:routed",
13
- channel: ctx.inboundMessage.channel,
14
- senderId: ctx.inboundMessage.sender.id,
15
- agentId: ctx.binding?.agentId,
16
- threadId: ctx.binding?.threadId,
17
- duration: Date.now() - start,
18
- status: "success",
19
- });
20
- } catch (error) {
21
- logger.error(
22
- error instanceof Error ? error.message : String(error),
23
- {
24
- event: "message:error",
25
- channel: ctx.inboundMessage.channel,
26
- senderId: ctx.inboundMessage.sender.id,
27
- duration: Date.now() - start,
28
- status: "error",
29
- },
30
- );
31
- throw error;
32
- }
33
- };
34
- }
@@ -1,38 +0,0 @@
1
- import type { MessageMiddleware } from "../MessageContext";
2
-
3
- const processedMessages = new Map<string, number>();
4
-
5
- export function createDeduplicationMiddleware(
6
- ttlMs: number = 5 * 60 * 1000,
7
- ): MessageMiddleware {
8
- return async (ctx, next) => {
9
- const msg = ctx.inboundMessage;
10
- const msgId = msg.content.metadata?.messageId as string | undefined;
11
- const key = msgId
12
- ? `${msg.channel}:${msg.channelInstallationId}:${msgId}`
13
- : null;
14
-
15
- if (key) {
16
- const now = Date.now();
17
- const lastProcessed = processedMessages.get(key);
18
- if (lastProcessed && (now - lastProcessed) < ttlMs) {
19
- console.warn({
20
- event: "dedup:blocked",
21
- channel: msg.channel,
22
- senderId: msg.sender.id,
23
- messageId: msgId,
24
- }, "Duplicate message blocked by deduplication");
25
- return;
26
- }
27
- processedMessages.set(key, now);
28
- }
29
-
30
- if (processedMessages.size > 10000) {
31
- const oldest = Array.from(processedMessages.entries())
32
- .sort((a, b) => a[1] - b[1])[0];
33
- if (oldest) processedMessages.delete(oldest[0]);
34
- }
35
-
36
- await next();
37
- };
38
- }
@@ -1,3 +0,0 @@
1
- export { createDeduplicationMiddleware } from "./deduplication";
2
- export { createRateLimitMiddleware } from "./rateLimit";
3
- export { createAuditLoggerMiddleware } from "./auditLogger";
@@ -1,46 +0,0 @@
1
- import type { MessageMiddleware } from "../MessageContext";
2
-
3
- export class RateLimitError extends Error {
4
- constructor(message: string) {
5
- super(message);
6
- this.name = "RateLimitError";
7
- }
8
- }
9
-
10
- interface Counter { count: number; resetAt: number; }
11
- const rateCounters = new Map<string, Counter>();
12
-
13
- export function createRateLimitMiddleware(
14
- maxRequests: number = 10,
15
- windowMs: number = 60 * 1000,
16
- maxEntries: number = 10000,
17
- ): MessageMiddleware {
18
- return async (ctx, next) => {
19
- const senderKey = `${ctx.inboundMessage.channel}:${ctx.inboundMessage.sender.id}`;
20
- const now = Date.now();
21
- let counter = rateCounters.get(senderKey);
22
- if (!counter || now > counter.resetAt) {
23
- counter = { count: 0, resetAt: now + windowMs };
24
- }
25
- counter.count++;
26
- rateCounters.set(senderKey, counter);
27
-
28
- if (rateCounters.size > maxEntries) {
29
- const oldest = Array.from(rateCounters.entries())
30
- .sort((a, b) => a[1].resetAt - b[1].resetAt)[0];
31
- if (oldest) rateCounters.delete(oldest[0]);
32
- }
33
-
34
- if (counter.count > maxRequests) {
35
- console.warn({
36
- event: "ratelimit:blocked",
37
- channel: ctx.inboundMessage.channel,
38
- senderId: ctx.inboundMessage.sender.id,
39
- count: counter.count,
40
- maxRequests,
41
- }, "Rate limit exceeded");
42
- throw new RateLimitError(`Rate limit exceeded`);
43
- }
44
- await next();
45
- };
46
- }
@@ -1,177 +0,0 @@
1
- import fastify, { FastifyInstance } from "fastify";
2
- import type { LocalRuntimeConfig } from "@axiom-lattice/protocols";
3
- import { registerA2ATestConnectionRoute } from "../a2a-test-connection";
4
-
5
- const mockStartProvider = jest.fn();
6
- const mockRemoveState = jest.fn();
7
-
8
- jest.mock("../../services/local-a2a", () => ({
9
- localA2AService: {
10
- startProvider: (...args: unknown[]) => mockStartProvider(...args),
11
- removeState: (...args: unknown[]) => mockRemoveState(...args),
12
- },
13
- }));
14
-
15
- describe("A2A test connection route", () => {
16
- let app: FastifyInstance;
17
- const originalFetch = global.fetch;
18
- const mockFetch = jest.fn<(...args: Parameters<typeof fetch>) => ReturnType<typeof fetch>>();
19
-
20
- const localRuntime: LocalRuntimeConfig = {
21
- label: "Local Test",
22
- agentCardUrl: "http://old-host/.well-known/agent-card.json",
23
- command: {
24
- executable: "local-a2a",
25
- args: ["--port", "4301"],
26
- },
27
- };
28
-
29
- beforeEach(() => {
30
- jest.clearAllMocks();
31
- global.fetch = mockFetch as unknown as typeof fetch;
32
- app = fastify();
33
- registerA2ATestConnectionRoute(app);
34
- });
35
-
36
- afterEach(async () => {
37
- await app.close();
38
- global.fetch = originalFetch;
39
- });
40
-
41
- it("starts a temporary local runtime before fetching the local agent card", async () => {
42
- mockStartProvider.mockResolvedValueOnce({
43
- runtimeId: "test-runtime",
44
- label: "Local Test",
45
- status: "running",
46
- enabled: true,
47
- });
48
- mockFetch.mockResolvedValueOnce(
49
- new Response(JSON.stringify({ name: "Local Agent" }), { status: 200 }),
50
- );
51
-
52
- const response = await app.inject({
53
- method: "POST",
54
- url: "/api/a2a/test-connection",
55
- payload: {
56
- agentCardUrl: "localhost:4301",
57
- localRuntime,
58
- },
59
- });
60
-
61
- expect(response.statusCode).toBe(200);
62
- const body = JSON.parse(response.body);
63
- expect(body.data.reachable).toBe(true);
64
- expect(body.data.agentName).toBe("Local Agent");
65
-
66
- expect(mockStartProvider).toHaveBeenCalledTimes(1);
67
- const [runtimeId, startedRuntime] = mockStartProvider.mock.calls[0] as [
68
- string,
69
- LocalRuntimeConfig,
70
- ];
71
- expect(runtimeId).toMatch(/^test-/);
72
- expect(startedRuntime.agentCardUrl).toBe(
73
- "http://localhost:4301/.well-known/agent-card.json",
74
- );
75
-
76
- expect(mockFetch).toHaveBeenCalledWith(
77
- "http://localhost:4301/.well-known/agent-card.json",
78
- expect.objectContaining({ headers: {} }),
79
- );
80
- expect(mockRemoveState).toHaveBeenCalledWith("test-runtime");
81
- });
82
-
83
- it("uses https by default for remote URL tests and does not start a local runtime", async () => {
84
- mockFetch.mockResolvedValueOnce(
85
- new Response(JSON.stringify({ name: "Remote Agent" }), { status: 200 }),
86
- );
87
-
88
- const response = await app.inject({
89
- method: "POST",
90
- url: "/api/a2a/test-connection",
91
- payload: {
92
- agentCardUrl: "example.com/a2a",
93
- apiKey: "secret",
94
- },
95
- });
96
-
97
- expect(response.statusCode).toBe(200);
98
- const body = JSON.parse(response.body);
99
- expect(body.data.reachable).toBe(true);
100
- expect(body.data.agentName).toBe("Remote Agent");
101
- expect(mockStartProvider).not.toHaveBeenCalled();
102
- expect(mockRemoveState).not.toHaveBeenCalled();
103
- expect(mockFetch).toHaveBeenCalledWith(
104
- "https://example.com/a2a/.well-known/agent-card.json",
105
- expect.objectContaining({
106
- headers: { Authorization: "Bearer secret" },
107
- }),
108
- );
109
- });
110
-
111
- it("does not fetch the agent card when the local runtime fails to start", async () => {
112
- mockStartProvider.mockResolvedValueOnce({
113
- runtimeId: "test-failed",
114
- label: "Local Test",
115
- status: "failed",
116
- enabled: true,
117
- message: "Server did not become ready",
118
- });
119
-
120
- const response = await app.inject({
121
- method: "POST",
122
- url: "/api/a2a/test-connection",
123
- payload: {
124
- agentCardUrl: "localhost:4301",
125
- localRuntime,
126
- },
127
- });
128
-
129
- expect(response.statusCode).toBe(200);
130
- const body = JSON.parse(response.body);
131
- expect(body.data.reachable).toBe(false);
132
- expect(body.data.error).toContain("Server did not become ready");
133
- expect(mockFetch).not.toHaveBeenCalled();
134
- expect(mockRemoveState).toHaveBeenCalledWith("test-failed");
135
- });
136
-
137
- it.each([
138
- ["--port with next arg", ["--port", "4302"]],
139
- ["--port inline", ["--port=4302"]],
140
- ["-p with next arg", ["-p", "4302"]],
141
- ["-p inline", ["-p=4302"]],
142
- ])("does not start local runtime when %s mismatches the agent card URL port", async (_label, args) => {
143
- const response = await app.inject({
144
- method: "POST",
145
- url: "/api/a2a/test-connection",
146
- payload: {
147
- agentCardUrl: "http://localhost:4301/.well-known/agent-card.json",
148
- localRuntime: {
149
- ...localRuntime,
150
- command: {
151
- executable: "local-a2a",
152
- args,
153
- },
154
- },
155
- },
156
- });
157
-
158
- expect(response.statusCode).toBe(200);
159
- const body = JSON.parse(response.body);
160
- expect(body.data.reachable).toBe(false);
161
- expect(body.data.error).toContain("command port (4302) does not match agentCardUrl port (4301)");
162
- expect(mockStartProvider).not.toHaveBeenCalled();
163
- expect(mockFetch).not.toHaveBeenCalled();
164
- });
165
-
166
- it("returns 400 when agentCardUrl is missing", async () => {
167
- const response = await app.inject({
168
- method: "POST",
169
- url: "/api/a2a/test-connection",
170
- payload: {},
171
- });
172
-
173
- expect(response.statusCode).toBe(400);
174
- expect(JSON.parse(response.body).error).toBe("agentCardUrl is required");
175
- expect(mockFetch).not.toHaveBeenCalled();
176
- });
177
- });
@@ -1,266 +0,0 @@
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
- }
@@ -1,126 +0,0 @@
1
- import { FastifyInstance } from "fastify";
2
- import { randomUUID } from "node:crypto";
3
- import type { AgentCard, LocalRuntimeConfig } from "@axiom-lattice/protocols";
4
- import { localA2AService } from "../services/local-a2a";
5
- import { normalizeA2AAgentCardUrl } from "../utils/a2a-url";
6
- import {
7
- validateLocalRuntimeCommand,
8
- validateLocalRuntimePort,
9
- } from "../utils/local-a2a-runtime";
10
-
11
- type TestConnectionError = Error & {
12
- code?: string;
13
- cause?: unknown;
14
- };
15
-
16
- function describeTestConnectionError(error: unknown, agentCardUrl: string): string {
17
- if (!(error instanceof Error)) {
18
- return `Failed to fetch agent card from ${agentCardUrl}: ${String(error)}`;
19
- }
20
-
21
- const cause = (error as TestConnectionError).cause;
22
- const causeParts: string[] = [];
23
- if (cause instanceof Error) {
24
- causeParts.push(cause.message);
25
- const causeCode = (cause as TestConnectionError).code;
26
- if (causeCode && !causeParts.some((part) => part.includes(causeCode))) {
27
- causeParts.push(causeCode);
28
- }
29
- } else if (typeof cause === "object" && cause !== null) {
30
- const code = "code" in cause ? String(cause.code) : "";
31
- const message = "message" in cause ? String(cause.message) : "";
32
- if (message) causeParts.push(message);
33
- if (code && !message.includes(code)) causeParts.push(code);
34
- }
35
-
36
- const detail = causeParts.length > 0 ? ` (${causeParts.join(", ")})` : "";
37
- return `Failed to fetch agent card from ${agentCardUrl}: ${error.message}${detail}`;
38
- }
39
-
40
- async function ensureLocalRuntimeForConnectionTest(
41
- localRuntime: LocalRuntimeConfig,
42
- agentCardUrl: string,
43
- ): Promise<{ runtimeId: string } | { error: string }> {
44
- const commandValidation = validateLocalRuntimeCommand(localRuntime);
45
- if (!commandValidation.ok) {
46
- return { error: commandValidation.error };
47
- }
48
- const testRuntime = {
49
- ...localRuntime,
50
- agentCardUrl,
51
- command: {
52
- ...localRuntime.command,
53
- executable: localRuntime.command.executable.trim(),
54
- },
55
- };
56
- const portValidation = validateLocalRuntimePort(testRuntime, agentCardUrl);
57
- if (!portValidation.ok) {
58
- return { error: portValidation.error };
59
- }
60
-
61
- const state = await localA2AService.startProvider(
62
- `test-${randomUUID()}`,
63
- testRuntime,
64
- );
65
- if (state.status === "running") return { runtimeId: state.runtimeId };
66
- const detail = state.message ? ` ${state.message}` : "";
67
- localA2AService.removeState(state.runtimeId);
68
- return { error: `Local managed A2A server could not start for connection test.${detail}` };
69
- }
70
-
71
- async function readShortResponseBody(resp: Response): Promise<string> {
72
- const body = await resp.text().catch(() => "");
73
- return body.length > 500 ? `${body.slice(0, 500)}...` : body;
74
- }
75
-
76
- export function registerA2ATestConnectionRoute(app: FastifyInstance): void {
77
- app.post("/api/a2a/test-connection", async (request, reply) => {
78
- const { agentCardUrl, apiKey, localRuntime } = (request.body ?? {}) as {
79
- agentCardUrl?: string;
80
- apiKey?: string;
81
- localRuntime?: LocalRuntimeConfig;
82
- };
83
- const normalizedAgentCardUrl = normalizeA2AAgentCardUrl(
84
- agentCardUrl,
85
- localRuntime ? "http://" : "https://",
86
- );
87
- if (!normalizedAgentCardUrl) {
88
- return reply.status(400).send({ success: false, error: "agentCardUrl is required" });
89
- }
90
-
91
- let testRuntimeId: string | null = null;
92
- try {
93
- const headers: Record<string, string> = {};
94
- if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`;
95
-
96
- if (localRuntime) {
97
- const localStartResult = await ensureLocalRuntimeForConnectionTest(localRuntime, normalizedAgentCardUrl);
98
- if ("error" in localStartResult) {
99
- return reply.send({ success: true, data: { reachable: false, error: localStartResult.error } });
100
- }
101
- testRuntimeId = localStartResult.runtimeId;
102
- }
103
-
104
- const resp = await fetch(normalizedAgentCardUrl, { headers, signal: AbortSignal.timeout(8000) });
105
- if (!resp.ok) {
106
- const body = await readShortResponseBody(resp);
107
- const suffix = body ? `: ${body}` : "";
108
- return reply.send({ success: true, data: { reachable: false, error: `HTTP ${resp.status} ${resp.statusText}${suffix}` } });
109
- }
110
- const card = await resp.json() as AgentCard;
111
- return reply.send({ success: true, data: { reachable: true, agentName: card.name, agentCard: card } });
112
- } catch (error: unknown) {
113
- return reply.send({
114
- success: true,
115
- data: {
116
- reachable: false,
117
- error: describeTestConnectionError(error, normalizedAgentCardUrl),
118
- },
119
- });
120
- } finally {
121
- if (testRuntimeId) {
122
- localA2AService.removeState(testRuntimeId);
123
- }
124
- }
125
- });
126
- }