@clawcrony/claw-crony 1.0.1

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 (107) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -0
  3. package/dist/index.d.ts +17 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +720 -0
  6. package/dist/index.js.map +1 -0
  7. package/dist/src/agent-card.d.ts +4 -0
  8. package/dist/src/agent-card.d.ts.map +1 -0
  9. package/dist/src/agent-card.js +61 -0
  10. package/dist/src/agent-card.js.map +1 -0
  11. package/dist/src/audit.d.ts +36 -0
  12. package/dist/src/audit.d.ts.map +1 -0
  13. package/dist/src/audit.js +88 -0
  14. package/dist/src/audit.js.map +1 -0
  15. package/dist/src/client.d.ts +53 -0
  16. package/dist/src/client.d.ts.map +1 -0
  17. package/dist/src/client.js +322 -0
  18. package/dist/src/client.js.map +1 -0
  19. package/dist/src/executor.d.ts +34 -0
  20. package/dist/src/executor.d.ts.map +1 -0
  21. package/dist/src/executor.js +994 -0
  22. package/dist/src/executor.js.map +1 -0
  23. package/dist/src/file-security.d.ts +63 -0
  24. package/dist/src/file-security.d.ts.map +1 -0
  25. package/dist/src/file-security.js +350 -0
  26. package/dist/src/file-security.js.map +1 -0
  27. package/dist/src/hub-match.d.ts +73 -0
  28. package/dist/src/hub-match.d.ts.map +1 -0
  29. package/dist/src/hub-match.js +120 -0
  30. package/dist/src/hub-match.js.map +1 -0
  31. package/dist/src/hub-registration.d.ts +24 -0
  32. package/dist/src/hub-registration.d.ts.map +1 -0
  33. package/dist/src/hub-registration.js +242 -0
  34. package/dist/src/hub-registration.js.map +1 -0
  35. package/dist/src/internal/envelope.d.ts +33 -0
  36. package/dist/src/internal/envelope.d.ts.map +1 -0
  37. package/dist/src/internal/envelope.js +152 -0
  38. package/dist/src/internal/envelope.js.map +1 -0
  39. package/dist/src/internal/idempotency.d.ts +48 -0
  40. package/dist/src/internal/idempotency.d.ts.map +1 -0
  41. package/dist/src/internal/idempotency.js +82 -0
  42. package/dist/src/internal/idempotency.js.map +1 -0
  43. package/dist/src/internal/metrics.d.ts +38 -0
  44. package/dist/src/internal/metrics.d.ts.map +1 -0
  45. package/dist/src/internal/metrics.js +83 -0
  46. package/dist/src/internal/metrics.js.map +1 -0
  47. package/dist/src/internal/outbox.d.ts +49 -0
  48. package/dist/src/internal/outbox.d.ts.map +1 -0
  49. package/dist/src/internal/outbox.js +149 -0
  50. package/dist/src/internal/outbox.js.map +1 -0
  51. package/dist/src/internal/routing.d.ts +28 -0
  52. package/dist/src/internal/routing.d.ts.map +1 -0
  53. package/dist/src/internal/routing.js +57 -0
  54. package/dist/src/internal/routing.js.map +1 -0
  55. package/dist/src/internal/security.d.ts +53 -0
  56. package/dist/src/internal/security.d.ts.map +1 -0
  57. package/dist/src/internal/security.js +122 -0
  58. package/dist/src/internal/security.js.map +1 -0
  59. package/dist/src/internal/transport.d.ts +49 -0
  60. package/dist/src/internal/transport.d.ts.map +1 -0
  61. package/dist/src/internal/transport.js +207 -0
  62. package/dist/src/internal/transport.js.map +1 -0
  63. package/dist/src/internal/types-internal.d.ts +95 -0
  64. package/dist/src/internal/types-internal.d.ts.map +1 -0
  65. package/dist/src/internal/types-internal.js +9 -0
  66. package/dist/src/internal/types-internal.js.map +1 -0
  67. package/dist/src/peer-health.d.ts +47 -0
  68. package/dist/src/peer-health.d.ts.map +1 -0
  69. package/dist/src/peer-health.js +169 -0
  70. package/dist/src/peer-health.js.map +1 -0
  71. package/dist/src/peer-retry.d.ts +16 -0
  72. package/dist/src/peer-retry.d.ts.map +1 -0
  73. package/dist/src/peer-retry.js +75 -0
  74. package/dist/src/peer-retry.js.map +1 -0
  75. package/dist/src/queueing-executor.d.ts +23 -0
  76. package/dist/src/queueing-executor.d.ts.map +1 -0
  77. package/dist/src/queueing-executor.js +179 -0
  78. package/dist/src/queueing-executor.js.map +1 -0
  79. package/dist/src/routing-rules.d.ts +53 -0
  80. package/dist/src/routing-rules.d.ts.map +1 -0
  81. package/dist/src/routing-rules.js +130 -0
  82. package/dist/src/routing-rules.js.map +1 -0
  83. package/dist/src/task-cleanup.d.ts +21 -0
  84. package/dist/src/task-cleanup.d.ts.map +1 -0
  85. package/dist/src/task-cleanup.js +77 -0
  86. package/dist/src/task-cleanup.js.map +1 -0
  87. package/dist/src/task-store.d.ts +16 -0
  88. package/dist/src/task-store.d.ts.map +1 -0
  89. package/dist/src/task-store.js +80 -0
  90. package/dist/src/task-store.js.map +1 -0
  91. package/dist/src/telemetry.d.ts +88 -0
  92. package/dist/src/telemetry.d.ts.map +1 -0
  93. package/dist/src/telemetry.js +235 -0
  94. package/dist/src/telemetry.js.map +1 -0
  95. package/dist/src/transport-fallback.d.ts +29 -0
  96. package/dist/src/transport-fallback.d.ts.map +1 -0
  97. package/dist/src/transport-fallback.js +81 -0
  98. package/dist/src/transport-fallback.js.map +1 -0
  99. package/dist/src/types.d.ts +160 -0
  100. package/dist/src/types.d.ts.map +1 -0
  101. package/dist/src/types.js +7 -0
  102. package/dist/src/types.js.map +1 -0
  103. package/openclaw.plugin.json +272 -0
  104. package/package.json +56 -0
  105. package/skill/SKILL.md +230 -0
  106. package/skill/references/tools-md-template.md +57 -0
  107. package/skill/scripts/a2a-send.mjs +357 -0
package/dist/index.js ADDED
@@ -0,0 +1,720 @@
1
+ /**
2
+ * A2A Gateway plugin endpoints:
3
+ * - /.well-known/agent.json (Agent Card discovery)
4
+ * - /a2a/jsonrpc (JSON-RPC transport)
5
+ * - /a2a/rest (REST transport)
6
+ * - gRPC on port+1 (gRPC transport)
7
+ */
8
+ import os from "node:os";
9
+ import path from "node:path";
10
+ import { AGENT_CARD_PATH } from "@a2a-js/sdk";
11
+ import { DefaultRequestHandler } from "@a2a-js/sdk/server";
12
+ import { UserBuilder, agentCardHandler, jsonRpcHandler, restHandler } from "@a2a-js/sdk/server/express";
13
+ import { grpcService, A2AService, UserBuilder as GrpcUserBuilder } from "@a2a-js/sdk/server/grpc";
14
+ import { Server as GrpcServer, ServerCredentials, status as GrpcStatus } from "@grpc/grpc-js";
15
+ import express from "express";
16
+ import { buildAgentCard } from "./src/agent-card.js";
17
+ import { A2AClient } from "./src/client.js";
18
+ import { OpenClawAgentExecutor } from "./src/executor.js";
19
+ import { QueueingAgentExecutor } from "./src/queueing-executor.js";
20
+ import { runTaskCleanup } from "./src/task-cleanup.js";
21
+ import { FileTaskStore } from "./src/task-store.js";
22
+ import { GatewayTelemetry } from "./src/telemetry.js";
23
+ import { AuditLogger } from "./src/audit.js";
24
+ import { PeerHealthManager } from "./src/peer-health.js";
25
+ import { runHubRegistration } from "./src/hub-registration.js";
26
+ import { HubMatchClient } from "./src/hub-match.js";
27
+ import { parseRoutingRules, matchRule } from "./src/routing-rules.js";
28
+ import { validateUri, validateMimeType, } from "./src/file-security.js";
29
+ /** Build a JSON-RPC error response. */
30
+ function jsonRpcError(id, code, message) {
31
+ return { jsonrpc: "2.0", id, error: { code, message } };
32
+ }
33
+ function asObject(value) {
34
+ if (!value || typeof value !== "object") {
35
+ return {};
36
+ }
37
+ return value;
38
+ }
39
+ function asString(value, fallback = "") {
40
+ if (typeof value === "string") {
41
+ return value;
42
+ }
43
+ return fallback;
44
+ }
45
+ function asNumber(value, fallback) {
46
+ if (typeof value === "number" && Number.isFinite(value)) {
47
+ return value;
48
+ }
49
+ return fallback;
50
+ }
51
+ function asBoolean(value, fallback) {
52
+ if (typeof value === "boolean") {
53
+ return value;
54
+ }
55
+ return fallback;
56
+ }
57
+ function normalizeHttpPath(value, fallback) {
58
+ const trimmed = value.trim() || fallback;
59
+ return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
60
+ }
61
+ function resolveConfiguredPath(value, fallback, resolvePath) {
62
+ const configured = asString(value, "").trim() || fallback;
63
+ const resolved = resolvePath ? resolvePath(configured) : configured;
64
+ return path.isAbsolute(resolved) ? resolved : path.resolve(resolved);
65
+ }
66
+ /** Extract skill names from an Agent Card object (used for routing-rules skill matching). */
67
+ function extractSkillsFromAgentCard(card) {
68
+ const skills = card.skills;
69
+ if (!Array.isArray(skills))
70
+ return [];
71
+ return skills.map((s) => (typeof s === "string" ? s : asObject(s).name ?? "")).filter(Boolean);
72
+ }
73
+ function parseAgentCard(raw) {
74
+ const skills = Array.isArray(raw.skills) ? raw.skills : [];
75
+ return {
76
+ name: asString(raw.name, "OpenClaw A2A Gateway"),
77
+ description: asString(raw.description, "A2A bridge for OpenClaw agents"),
78
+ url: asString(raw.url, ""),
79
+ skills: skills.map((entry) => {
80
+ if (typeof entry === "string") {
81
+ return entry;
82
+ }
83
+ const skill = asObject(entry);
84
+ return {
85
+ id: asString(skill.id, ""),
86
+ name: asString(skill.name, "unknown"),
87
+ description: asString(skill.description, ""),
88
+ };
89
+ }),
90
+ };
91
+ }
92
+ function parsePeers(raw) {
93
+ if (!Array.isArray(raw)) {
94
+ return [];
95
+ }
96
+ const peers = [];
97
+ for (const entry of raw) {
98
+ const value = asObject(entry);
99
+ const name = asString(value.name, "");
100
+ const agentCardUrl = asString(value.agentCardUrl, "");
101
+ if (!name || !agentCardUrl) {
102
+ continue;
103
+ }
104
+ const authRaw = asObject(value.auth);
105
+ const authTypeRaw = asString(authRaw.type, "");
106
+ const authType = authTypeRaw === "bearer" || authTypeRaw === "apiKey" ? authTypeRaw : "";
107
+ const token = asString(authRaw.token, "");
108
+ peers.push({
109
+ name,
110
+ agentCardUrl,
111
+ auth: authType && token ? { type: authType, token } : undefined,
112
+ });
113
+ }
114
+ return peers;
115
+ }
116
+ export function parseConfig(raw, resolvePath) {
117
+ const config = asObject(raw);
118
+ const server = asObject(config.server);
119
+ const storage = asObject(config.storage);
120
+ const security = asObject(config.security);
121
+ const routing = asObject(config.routing);
122
+ const limits = asObject(config.limits);
123
+ const observability = asObject(config.observability);
124
+ const timeouts = asObject(config.timeouts);
125
+ const resilience = asObject(config.resilience);
126
+ const healthCheck = asObject(resilience.healthCheck);
127
+ const retry = asObject(resilience.retry);
128
+ const circuitBreaker = asObject(resilience.circuitBreaker);
129
+ const hub = asObject(config.hub);
130
+ const registration = asObject(config.registration);
131
+ const inboundAuth = asString(security.inboundAuth, "none");
132
+ const defaultMimeTypes = [
133
+ "image/*", "application/pdf", "text/plain", "text/csv",
134
+ "application/json", "audio/*", "video/*",
135
+ ];
136
+ const rawAllowedMime = Array.isArray(security.allowedMimeTypes) ? security.allowedMimeTypes : [];
137
+ const allowedMimeTypes = rawAllowedMime.length > 0
138
+ ? rawAllowedMime.filter((v) => typeof v === "string")
139
+ : defaultMimeTypes;
140
+ const rawUriAllowlist = Array.isArray(security.fileUriAllowlist) ? security.fileUriAllowlist : [];
141
+ const fileUriAllowlist = rawUriAllowlist.filter((v) => typeof v === "string");
142
+ return {
143
+ agentCard: parseAgentCard(asObject(config.agentCard)),
144
+ server: {
145
+ host: asString(server.host, "0.0.0.0"),
146
+ port: asNumber(server.port, 18800),
147
+ },
148
+ storage: {
149
+ tasksDir: resolveConfiguredPath(storage.tasksDir, path.join(os.homedir(), ".openclaw", "a2a-tasks"), resolvePath),
150
+ taskTtlHours: Math.max(1, asNumber(storage.taskTtlHours, 72)),
151
+ cleanupIntervalMinutes: Math.max(1, asNumber(storage.cleanupIntervalMinutes, 60)),
152
+ },
153
+ peers: parsePeers(config.peers),
154
+ security: (() => {
155
+ const singleToken = asString(security.token, "");
156
+ const tokenArray = Array.isArray(security.tokens)
157
+ ? security.tokens.filter((t) => typeof t === "string" && t.length > 0)
158
+ : [];
159
+ const validTokens = new Set([singleToken, ...tokenArray].filter(t => t.length > 0));
160
+ return {
161
+ inboundAuth: inboundAuth === "bearer" ? "bearer" : "none",
162
+ token: singleToken,
163
+ tokens: tokenArray,
164
+ validTokens,
165
+ allowedMimeTypes,
166
+ maxFileSizeBytes: asNumber(security.maxFileSizeBytes, 52_428_800),
167
+ maxInlineFileSizeBytes: asNumber(security.maxInlineFileSizeBytes, 10_485_760),
168
+ fileUriAllowlist,
169
+ };
170
+ })(),
171
+ routing: {
172
+ defaultAgentId: asString(routing.defaultAgentId, "default"),
173
+ rules: parseRoutingRules(routing.rules),
174
+ },
175
+ limits: {
176
+ maxConcurrentTasks: Math.max(1, Math.floor(asNumber(limits.maxConcurrentTasks, 4))),
177
+ maxQueuedTasks: Math.max(0, Math.floor(asNumber(limits.maxQueuedTasks, 100))),
178
+ },
179
+ observability: {
180
+ structuredLogs: asBoolean(observability.structuredLogs, true),
181
+ exposeMetricsEndpoint: asBoolean(observability.exposeMetricsEndpoint, true),
182
+ metricsPath: normalizeHttpPath(asString(observability.metricsPath, "/a2a/metrics"), "/a2a/metrics"),
183
+ metricsAuth: (asString(observability.metricsAuth, "none") === "bearer" ? "bearer" : "none"),
184
+ auditLogPath: resolveConfiguredPath(observability.auditLogPath, path.join(os.homedir(), ".openclaw", "a2a-audit.jsonl"), resolvePath),
185
+ },
186
+ timeouts: {
187
+ agentResponseTimeoutMs: asNumber(timeouts.agentResponseTimeoutMs, 300_000),
188
+ },
189
+ resilience: {
190
+ healthCheck: {
191
+ enabled: asBoolean(healthCheck.enabled, true),
192
+ intervalMs: asNumber(healthCheck.intervalMs, 30_000),
193
+ timeoutMs: asNumber(healthCheck.timeoutMs, 5_000),
194
+ },
195
+ retry: {
196
+ maxRetries: Math.max(0, Math.floor(asNumber(retry.maxRetries, 3))),
197
+ baseDelayMs: asNumber(retry.baseDelayMs, 1_000),
198
+ maxDelayMs: asNumber(retry.maxDelayMs, 10_000),
199
+ },
200
+ circuitBreaker: {
201
+ failureThreshold: Math.max(1, Math.floor(asNumber(circuitBreaker.failureThreshold, 5))),
202
+ resetTimeoutMs: asNumber(circuitBreaker.resetTimeoutMs, 30_000),
203
+ },
204
+ },
205
+ hub: {
206
+ url: asString(hub.url, "https://www.factormining.cn"),
207
+ enabled: asBoolean(hub.enabled, true),
208
+ registrationEnabled: asBoolean(hub.registrationEnabled, true),
209
+ },
210
+ registration: {
211
+ username: asString(registration.username, ""),
212
+ email: asString(registration.email, ""),
213
+ },
214
+ };
215
+ }
216
+ function normalizeCardPath() {
217
+ if (AGENT_CARD_PATH.startsWith("/")) {
218
+ return AGENT_CARD_PATH;
219
+ }
220
+ return `/${AGENT_CARD_PATH}`;
221
+ }
222
+ const plugin = {
223
+ id: "a2a-gateway",
224
+ name: "A2A Gateway",
225
+ description: "OpenClaw plugin that serves A2A v0.3.0 endpoints",
226
+ register(api) {
227
+ const config = parseConfig(api.pluginConfig, api.resolvePath?.bind(api));
228
+ const telemetry = new GatewayTelemetry(api.logger, {
229
+ structuredLogs: config.observability.structuredLogs,
230
+ });
231
+ const auditLogger = new AuditLogger(config.observability.auditLogPath);
232
+ const client = new A2AClient();
233
+ const taskStore = new FileTaskStore(config.storage.tasksDir);
234
+ const executor = new QueueingAgentExecutor(new OpenClawAgentExecutor(api, config), telemetry, config.limits);
235
+ const agentCard = buildAgentCard(config);
236
+ // Peer resilience: health check + circuit breaker
237
+ const healthManager = config.peers.length > 0
238
+ ? new PeerHealthManager(config.peers, config.resilience.healthCheck, config.resilience.circuitBreaker, async (peer) => {
239
+ try {
240
+ const card = await client.discoverAgentCard(peer, config.resilience.healthCheck.timeoutMs);
241
+ // Cache peer's skills for routing-rules skill matching
242
+ const skills = extractSkillsFromAgentCard(card);
243
+ healthManager.setPeerSkills(peer.name, skills);
244
+ return true;
245
+ }
246
+ catch {
247
+ return false;
248
+ }
249
+ }, (level, msg, details) => {
250
+ if (level === "error") {
251
+ api.logger.error(details ? `${msg}: ${JSON.stringify(details)}` : msg);
252
+ }
253
+ else if (level === "warn") {
254
+ api.logger.warn(details ? `${msg}: ${JSON.stringify(details)}` : msg);
255
+ }
256
+ else {
257
+ api.logger.info(details ? `${msg}: ${JSON.stringify(details)}` : msg);
258
+ }
259
+ })
260
+ : null;
261
+ // Wire peer state into telemetry snapshot
262
+ if (healthManager) {
263
+ telemetry.setPeerStateProvider(() => healthManager.getAllStates());
264
+ }
265
+ // Wire audit logger for inbound task completion
266
+ telemetry.setTaskAuditCallback((taskId, contextId, state, durationMs) => {
267
+ auditLogger.recordInbound(taskId, contextId, state, durationMs);
268
+ });
269
+ // SDK expects userBuilder(req) -> Promise<User>
270
+ // When bearer auth is configured, validate the Authorization header.
271
+ const userBuilder = async (req) => {
272
+ if (config.security.inboundAuth === "bearer" && config.security.validTokens.size > 0) {
273
+ const authHeader = req.headers?.authorization;
274
+ const header = Array.isArray(authHeader) ? authHeader[0] : authHeader;
275
+ const providedToken = typeof header === "string" && header.startsWith("Bearer ") ? header.slice(7) : "";
276
+ if (!providedToken || !config.security.validTokens.has(providedToken)) {
277
+ telemetry.recordSecurityRejection("http", "invalid or missing bearer token");
278
+ auditLogger.recordSecurityEvent("http", "invalid or missing bearer token");
279
+ throw jsonRpcError(null, -32000, "Unauthorized: invalid or missing bearer token");
280
+ }
281
+ }
282
+ return UserBuilder.noAuthentication();
283
+ };
284
+ const requestHandler = new DefaultRequestHandler(agentCard, taskStore, executor);
285
+ const app = express();
286
+ const createHttpMetricsMiddleware = (route) => (_req, res, next) => {
287
+ const startedAt = Date.now();
288
+ res.on("finish", () => {
289
+ telemetry.recordInboundHttp(route, res.statusCode, Date.now() - startedAt);
290
+ });
291
+ next();
292
+ };
293
+ const cardPath = normalizeCardPath();
294
+ const cardEndpointHandler = agentCardHandler({ agentCardProvider: requestHandler });
295
+ app.use(cardPath, cardEndpointHandler);
296
+ if (cardPath != "/.well-known/agent.json") {
297
+ app.use("/.well-known/agent.json", cardEndpointHandler);
298
+ }
299
+ app.use("/a2a/jsonrpc", createHttpMetricsMiddleware("jsonrpc"), jsonRpcHandler({
300
+ requestHandler,
301
+ userBuilder,
302
+ }));
303
+ // Ensure errors return JSON-RPC style responses (avoid Express HTML error pages)
304
+ app.use("/a2a/jsonrpc", (err, _req, res, next) => {
305
+ if (err instanceof SyntaxError) {
306
+ res.status(400).json(jsonRpcError(null, -32700, "Parse error"));
307
+ return;
308
+ }
309
+ // Surface A2A-specific errors with proper codes
310
+ const a2aErr = err;
311
+ if (a2aErr && typeof a2aErr.code === "number") {
312
+ const status = a2aErr.code === -32601 ? 404 : 400;
313
+ res.status(status).json(jsonRpcError(null, a2aErr.code, a2aErr.message || "Unknown error"));
314
+ return;
315
+ }
316
+ // Generic internal error
317
+ res.status(500).json(jsonRpcError(null, -32603, "Internal error"));
318
+ });
319
+ app.use("/a2a/rest", createHttpMetricsMiddleware("rest"), restHandler({
320
+ requestHandler,
321
+ userBuilder,
322
+ }));
323
+ if (config.observability.exposeMetricsEndpoint) {
324
+ app.get(config.observability.metricsPath, createHttpMetricsMiddleware("metrics"), (req, res, next) => {
325
+ if (config.observability.metricsAuth === "bearer" && config.security.validTokens.size > 0) {
326
+ const authHeader = req.headers.authorization;
327
+ const header = Array.isArray(authHeader) ? authHeader[0] : authHeader;
328
+ const token = typeof header === "string" && header.startsWith("Bearer ") ? header.slice(7) : "";
329
+ if (!token || !config.security.validTokens.has(token)) {
330
+ res.status(401).json({ error: "Unauthorized: invalid or missing bearer token" });
331
+ return;
332
+ }
333
+ }
334
+ next();
335
+ }, (_req, res) => {
336
+ res.json(telemetry.snapshot());
337
+ });
338
+ }
339
+ let server = null;
340
+ let grpcServer = null;
341
+ let cleanupTimer = null;
342
+ const grpcPort = config.server.port + 1;
343
+ api.registerGatewayMethod("a2a.metrics", ({ respond }) => {
344
+ respond(true, {
345
+ metrics: telemetry.snapshot(),
346
+ });
347
+ });
348
+ api.registerGatewayMethod("a2a.audit", ({ params, respond }) => {
349
+ const payload = asObject(params);
350
+ const count = Math.min(Math.max(1, asNumber(payload.count, 50)), 500);
351
+ auditLogger
352
+ .tail(count)
353
+ .then((entries) => respond(true, { entries, count: entries.length }))
354
+ .catch((error) => respond(false, { error: String(error?.message || error) }));
355
+ });
356
+ api.registerGatewayMethod("a2a.send", ({ params, respond }) => {
357
+ const payload = asObject(params);
358
+ const peerName = asString(payload.peer || payload.name, "");
359
+ const message = asObject(payload.message || payload.payload);
360
+ // Determine target peer: explicit name > routing rules > error
361
+ let resolvedPeerName = peerName;
362
+ let resolvedAgentId;
363
+ if (!resolvedPeerName && config.routing.rules && config.routing.rules.length > 0) {
364
+ const messageText = asString(message.text || message.message || "", "");
365
+ const messageTags = Array.isArray(message.tags) ? message.tags : undefined;
366
+ const peerSkills = healthManager?.getPeerSkills();
367
+ const routingMatch = matchRule(config.routing.rules, { text: messageText, tags: messageTags }, peerSkills);
368
+ if (routingMatch) {
369
+ resolvedPeerName = routingMatch.peer;
370
+ resolvedAgentId = routingMatch.agentId;
371
+ api.logger.info(`routing.match: "${resolvedPeerName}" via rule (agentId=${resolvedAgentId ?? "default"})`);
372
+ }
373
+ }
374
+ const peer = config.peers.find((candidate) => candidate.name === resolvedPeerName);
375
+ if (!peer) {
376
+ respond(false, { error: `Peer not found: ${resolvedPeerName || "(none)"}` });
377
+ return;
378
+ }
379
+ // Apply routing-rule agentId if not already set on the message
380
+ if (resolvedAgentId && !message.agentId) {
381
+ message.agentId = resolvedAgentId;
382
+ }
383
+ const startedAt = Date.now();
384
+ const sendOptions = {
385
+ healthManager: healthManager ?? undefined,
386
+ retryConfig: config.resilience.retry,
387
+ log: (level, msg, details) => {
388
+ if (details?.attempt) {
389
+ telemetry.recordPeerRetry(peer.name, details.attempt);
390
+ }
391
+ api.logger[level](details ? `${msg}: ${JSON.stringify(details)}` : msg);
392
+ },
393
+ };
394
+ client
395
+ .sendMessage(peer, message, sendOptions)
396
+ .then((result) => {
397
+ const outDuration = Date.now() - startedAt;
398
+ telemetry.recordOutboundRequest(peer.name, result.ok, result.statusCode, outDuration);
399
+ auditLogger.recordOutbound(peer.name, result.ok, result.statusCode, outDuration);
400
+ if (result.ok) {
401
+ respond(true, {
402
+ statusCode: result.statusCode,
403
+ response: result.response,
404
+ });
405
+ return;
406
+ }
407
+ respond(false, {
408
+ statusCode: result.statusCode,
409
+ response: result.response,
410
+ });
411
+ })
412
+ .catch((error) => {
413
+ const errDuration = Date.now() - startedAt;
414
+ telemetry.recordOutboundRequest(peer.name, false, 500, errDuration);
415
+ auditLogger.recordOutbound(peer.name, false, 500, errDuration);
416
+ respond(false, { error: String(error?.message || error) });
417
+ });
418
+ });
419
+ // ------------------------------------------------------------------
420
+ // Agent tool: a2a_send_file
421
+ // Lets the agent send a file (by URI) to a peer via A2A FilePart.
422
+ // ------------------------------------------------------------------
423
+ if (api.registerTool) {
424
+ const sendFileParams = {
425
+ type: "object",
426
+ required: ["peer", "uri"],
427
+ properties: {
428
+ peer: { type: "string", description: "Name of the target peer (must match a configured peer name)" },
429
+ uri: { type: "string", description: "Public URL of the file to send" },
430
+ name: { type: "string", description: "Filename (e.g. report.pdf)" },
431
+ mimeType: { type: "string", description: "MIME type (e.g. application/pdf). Auto-detected from extension if omitted." },
432
+ text: { type: "string", description: "Optional text message to include alongside the file" },
433
+ agentId: { type: "string", description: "Route to a specific agentId on the peer (OpenClaw extension). Omit to use the peer's default agent." },
434
+ },
435
+ };
436
+ api.registerTool({
437
+ name: "a2a_send_file",
438
+ description: "Send a file to a peer agent via A2A. The file is referenced by its public URL (URI). " +
439
+ "Use this when you need to transfer a document, image, or any file to another agent.",
440
+ label: "A2A Send File",
441
+ parameters: sendFileParams,
442
+ async execute(toolCallId, params) {
443
+ const peer = config.peers.find((p) => p.name === params.peer);
444
+ if (!peer) {
445
+ const available = config.peers.map((p) => p.name).join(", ") || "(none)";
446
+ return {
447
+ content: [{ type: "text", text: `Peer not found: "${params.peer}". Available peers: ${available}` }],
448
+ details: { ok: false },
449
+ };
450
+ }
451
+ // Security checks: SSRF, MIME, file size
452
+ const uriCheck = await validateUri(params.uri, config.security);
453
+ if (!uriCheck.ok) {
454
+ return {
455
+ content: [{ type: "text", text: `URI rejected: ${uriCheck.reason}` }],
456
+ details: { ok: false, reason: uriCheck.reason },
457
+ };
458
+ }
459
+ if (params.mimeType && !validateMimeType(params.mimeType, config.security.allowedMimeTypes)) {
460
+ return {
461
+ content: [{ type: "text", text: `MIME type rejected: "${params.mimeType}" is not in the allowed list` }],
462
+ details: { ok: false },
463
+ };
464
+ }
465
+ const parts = [];
466
+ if (params.text) {
467
+ parts.push({ kind: "text", text: params.text });
468
+ }
469
+ parts.push({
470
+ kind: "file",
471
+ file: {
472
+ uri: params.uri,
473
+ ...(params.name ? { name: params.name } : {}),
474
+ ...(params.mimeType ? { mimeType: params.mimeType } : {}),
475
+ },
476
+ });
477
+ try {
478
+ const message = { parts };
479
+ if (params.agentId) {
480
+ message.agentId = params.agentId;
481
+ }
482
+ const result = await client.sendMessage(peer, message, {
483
+ healthManager: healthManager ?? undefined,
484
+ retryConfig: config.resilience.retry,
485
+ });
486
+ if (result.ok) {
487
+ return {
488
+ content: [{ type: "text", text: `File sent to ${params.peer} via A2A.\nURI: ${params.uri}\nResponse: ${JSON.stringify(result.response)}` }],
489
+ details: { ok: true, response: result.response },
490
+ };
491
+ }
492
+ return {
493
+ content: [{ type: "text", text: `Failed to send file to ${params.peer}: ${JSON.stringify(result.response)}` }],
494
+ details: { ok: false, response: result.response },
495
+ };
496
+ }
497
+ catch (err) {
498
+ const msg = err instanceof Error ? err.message : String(err);
499
+ return {
500
+ content: [{ type: "text", text: `Error sending file to ${params.peer}: ${msg}` }],
501
+ details: { ok: false, error: msg },
502
+ };
503
+ }
504
+ },
505
+ });
506
+ // ------------------------------------------------------------------
507
+ // Agent tool: a2a_match_request
508
+ // Creates a match request on the hub and submits this agent's token.
509
+ // Returns provider address + yourToken + peerToken for A2A communication.
510
+ // ------------------------------------------------------------------
511
+ api.registerTool({
512
+ name: "a2a_match_request",
513
+ description: "Request a match with another agent via the hub. " +
514
+ "The hub finds a provider agent with matching skills, creates a match record, " +
515
+ "and returns the provider's address along with tokens for secure A2A communication. " +
516
+ "Use this to discover and connect with peer agents through the hub's registry.",
517
+ label: "A2A Match Request",
518
+ parameters: {
519
+ type: "object",
520
+ required: ["skills"],
521
+ properties: {
522
+ skills: {
523
+ type: "array",
524
+ items: { type: "string" },
525
+ description: "List of skill names to search for in a provider agent",
526
+ },
527
+ description: {
528
+ type: "string",
529
+ description: "Optional description of what you need from the provider",
530
+ },
531
+ },
532
+ },
533
+ async execute(toolCallId, params) {
534
+ let client;
535
+ try {
536
+ client = await HubMatchClient.create();
537
+ }
538
+ catch (err) {
539
+ const msg = err instanceof Error ? err.message : String(err);
540
+ return {
541
+ content: [{ type: "text", text: `Not registered with hub: ${msg}` }],
542
+ details: { ok: false, error: msg },
543
+ };
544
+ }
545
+ let match;
546
+ try {
547
+ match = await client.createMatch({
548
+ skills: params.skills,
549
+ description: params.description,
550
+ token: client["registration"].token,
551
+ });
552
+ }
553
+ catch (err) {
554
+ const msg = err instanceof Error ? err.message : String(err);
555
+ return {
556
+ content: [{ type: "text", text: `Failed to create match: ${msg}` }],
557
+ details: { ok: false, error: msg },
558
+ };
559
+ }
560
+ // Submit our token
561
+ let updatedMatch;
562
+ try {
563
+ updatedMatch = await client.submitToken(match.id, client["registration"].token);
564
+ }
565
+ catch (err) {
566
+ const msg = err instanceof Error ? err.message : String(err);
567
+ return {
568
+ content: [{ type: "text", text: `Match created (id=${match.id}) but failed to submit token: ${msg}` }],
569
+ details: { ok: false, matchId: match.id, error: msg },
570
+ };
571
+ }
572
+ const provider = updatedMatch.provider;
573
+ const providerAddress = provider?.address ?? "(unknown)";
574
+ const yourToken = updatedMatch.yourToken ?? "(none)";
575
+ const peerToken = updatedMatch.peerToken ?? "(none)";
576
+ const status = updatedMatch.status;
577
+ return {
578
+ content: [{
579
+ type: "text",
580
+ text: `Match ${status}: id=${updatedMatch.id}\n` +
581
+ `Provider: ${provider?.name ?? "(unknown)"} at ${providerAddress}\n` +
582
+ `Your token (use to contact provider): ${yourToken}\n` +
583
+ `Peer token (provider's token to contact you): ${peerToken}`,
584
+ }],
585
+ details: {
586
+ ok: true,
587
+ matchId: updatedMatch.id,
588
+ status: updatedMatch.status,
589
+ providerAddress,
590
+ yourToken,
591
+ peerToken,
592
+ },
593
+ };
594
+ },
595
+ });
596
+ }
597
+ if (!api.registerService) {
598
+ api.logger.warn("a2a-gateway: registerService is unavailable; HTTP endpoints are not started");
599
+ return;
600
+ }
601
+ api.registerService({
602
+ id: "a2a-gateway",
603
+ async start(_ctx) {
604
+ if (server) {
605
+ return;
606
+ }
607
+ // Hub registration (runs before server starts)
608
+ if (config.hub?.enabled !== false && config.hub?.registrationEnabled !== false) {
609
+ try {
610
+ const reg = await runHubRegistration(api, config, config.hub, config.registration ?? {});
611
+ if (reg) {
612
+ api.logger.info(`a2a-gateway: registered with hub (agentId=${reg.agentId})`);
613
+ }
614
+ }
615
+ catch (err) {
616
+ api.logger.warn(`a2a-gateway: hub registration failed — ${err instanceof Error ? err.message : String(err)}`);
617
+ // Continue startup anyway — hub is optional
618
+ }
619
+ }
620
+ // Start peer health checks
621
+ healthManager?.start();
622
+ // Start HTTP server (JSON-RPC + REST)
623
+ await new Promise((resolve, reject) => {
624
+ server = app.listen(config.server.port, config.server.host, () => {
625
+ api.logger.info(`a2a-gateway: HTTP listening on ${config.server.host}:${config.server.port}`);
626
+ api.logger.info(`a2a-gateway: durable task store at ${config.storage.tasksDir}; concurrency=${config.limits.maxConcurrentTasks}; queue=${config.limits.maxQueuedTasks}`);
627
+ resolve();
628
+ });
629
+ server.once("error", reject);
630
+ });
631
+ // Start gRPC server
632
+ try {
633
+ grpcServer = new GrpcServer();
634
+ const grpcUserBuilder = async (call) => {
635
+ if (config.security.inboundAuth === "bearer" && config.security.validTokens.size > 0) {
636
+ const meta = call?.metadata;
637
+ const values = meta?.get?.("authorization") || meta?.get?.("Authorization") || [];
638
+ const header = Array.isArray(values) && values.length > 0 ? String(values[0]) : "";
639
+ const providedToken = header.startsWith("Bearer ") ? header.slice(7) : "";
640
+ if (!providedToken || !config.security.validTokens.has(providedToken)) {
641
+ telemetry.recordSecurityRejection("grpc", "invalid or missing bearer token");
642
+ auditLogger.recordSecurityEvent("grpc", "invalid or missing bearer token");
643
+ const err = new Error("Unauthorized: invalid or missing bearer token");
644
+ err.code = GrpcStatus.UNAUTHENTICATED;
645
+ throw err;
646
+ }
647
+ }
648
+ return GrpcUserBuilder.noAuthentication();
649
+ };
650
+ grpcServer.addService(A2AService, grpcService({ requestHandler, userBuilder: grpcUserBuilder }));
651
+ await new Promise((resolve, reject) => {
652
+ grpcServer.bindAsync(`${config.server.host}:${grpcPort}`, ServerCredentials.createInsecure(), (error) => {
653
+ if (error) {
654
+ api.logger.warn(`a2a-gateway: gRPC failed to start: ${error.message}`);
655
+ grpcServer = null;
656
+ resolve(); // Non-fatal: HTTP still works
657
+ return;
658
+ }
659
+ try {
660
+ grpcServer.start();
661
+ }
662
+ catch {
663
+ // ignore: some grpc-js versions auto-start
664
+ }
665
+ api.logger.info(`a2a-gateway: gRPC listening on ${config.server.host}:${grpcPort}`);
666
+ resolve();
667
+ });
668
+ });
669
+ }
670
+ catch (grpcError) {
671
+ const msg = grpcError instanceof Error ? grpcError.message : String(grpcError);
672
+ api.logger.warn(`a2a-gateway: gRPC init failed: ${msg}`);
673
+ grpcServer = null;
674
+ }
675
+ // Start task TTL cleanup
676
+ const ttlMs = config.storage.taskTtlHours * 3_600_000;
677
+ const intervalMs = config.storage.cleanupIntervalMinutes * 60_000;
678
+ const doCleanup = () => {
679
+ void runTaskCleanup(taskStore, ttlMs, telemetry, api.logger);
680
+ };
681
+ // Run once at startup to clear any backlog
682
+ doCleanup();
683
+ cleanupTimer = setInterval(doCleanup, intervalMs);
684
+ api.logger.info(`a2a-gateway: task cleanup enabled — ttl=${config.storage.taskTtlHours}h interval=${config.storage.cleanupIntervalMinutes}min`);
685
+ },
686
+ async stop(_ctx) {
687
+ // Stop peer health checks
688
+ healthManager?.stop();
689
+ auditLogger.close();
690
+ // Stop task cleanup timer
691
+ if (cleanupTimer) {
692
+ clearInterval(cleanupTimer);
693
+ cleanupTimer = null;
694
+ }
695
+ // Stop gRPC server
696
+ if (grpcServer) {
697
+ grpcServer.forceShutdown();
698
+ grpcServer = null;
699
+ }
700
+ // Stop HTTP server
701
+ if (!server) {
702
+ return;
703
+ }
704
+ await new Promise((resolve, reject) => {
705
+ const activeServer = server;
706
+ server = null;
707
+ activeServer.close((error) => {
708
+ if (error) {
709
+ reject(error);
710
+ return;
711
+ }
712
+ resolve();
713
+ });
714
+ });
715
+ },
716
+ });
717
+ },
718
+ };
719
+ export default plugin;
720
+ //# sourceMappingURL=index.js.map