@clawcrony/claw-crony 1.2.3 → 1.3.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/CHANGELOG.md +107 -0
- package/CONFIG.md +341 -0
- package/README.md +154 -23
- package/dist/index.js +610 -112
- package/dist/src/ephemeral-token.d.ts +7 -0
- package/dist/src/ephemeral-token.js +17 -0
- package/dist/src/handshake-crypto.d.ts +3 -0
- package/dist/src/handshake-crypto.js +58 -0
- package/dist/src/history.d.ts +44 -0
- package/dist/src/history.js +119 -0
- package/dist/src/hub-match.d.ts +35 -46
- package/dist/src/hub-match.js +38 -53
- package/dist/src/hub-registration.d.ts +3 -10
- package/dist/src/hub-registration.js +33 -132
- package/dist/src/identity-store.d.ts +4 -0
- package/dist/src/identity-store.js +55 -0
- package/dist/src/types.d.ts +44 -3
- package/openclaw.plugin.json +46 -18
- package/package.json +43 -27
- package/scripts/a2a-diagnose.ps1 +15 -0
- package/scripts/a2a-diagnose.sh +22 -0
- package/scripts/a2a-history.ps1 +21 -0
- package/scripts/a2a-history.sh +9 -0
- package/scripts/a2a-match.ps1 +16 -0
- package/scripts/a2a-match.sh +13 -0
- package/scripts/a2a-peers.ps1 +1 -0
- package/scripts/a2a-peers.sh +4 -0
- package/scripts/a2a-send.ps1 +23 -0
- package/scripts/a2a-send.sh +14 -0
- package/scripts/a2a-update.ps1 +3 -0
- package/scripts/a2a-update.sh +6 -0
package/dist/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* - /a2a/rest (REST transport)
|
|
6
6
|
* - gRPC on port+1 (gRPC transport)
|
|
7
7
|
*/
|
|
8
|
+
import crypto from "node:crypto";
|
|
8
9
|
import os from "node:os";
|
|
9
10
|
import path from "node:path";
|
|
10
11
|
import { AGENT_CARD_PATH } from "@a2a-js/sdk";
|
|
@@ -21,11 +22,15 @@ import { runTaskCleanup } from "./src/task-cleanup.js";
|
|
|
21
22
|
import { FileTaskStore } from "./src/task-store.js";
|
|
22
23
|
import { GatewayTelemetry } from "./src/telemetry.js";
|
|
23
24
|
import { AuditLogger } from "./src/audit.js";
|
|
25
|
+
import { RequestHistoryStore } from "./src/history.js";
|
|
24
26
|
import { PeerHealthManager } from "./src/peer-health.js";
|
|
25
27
|
import { runHubRegistration } from "./src/hub-registration.js";
|
|
26
28
|
import { HubMatchClient } from "./src/hub-match.js";
|
|
27
29
|
import { normalizeAgentCardSkills } from "./src/skill-catalog.js";
|
|
28
30
|
import { parseRoutingRules, matchRule } from "./src/routing-rules.js";
|
|
31
|
+
import { decryptHandshake, encryptHandshake } from "./src/handshake-crypto.js";
|
|
32
|
+
import { isValidEphemeralInboundToken, issueEphemeralInboundToken } from "./src/ephemeral-token.js";
|
|
33
|
+
import { loadIdentity } from "./src/identity-store.js";
|
|
29
34
|
import { validateUri, validateMimeType, } from "./src/file-security.js";
|
|
30
35
|
/** Build a JSON-RPC error response. */
|
|
31
36
|
function jsonRpcError(id, code, message) {
|
|
@@ -183,6 +188,9 @@ export function parseConfig(raw, resolvePath) {
|
|
|
183
188
|
metricsPath: normalizeHttpPath(asString(observability.metricsPath, "/a2a/metrics"), "/a2a/metrics"),
|
|
184
189
|
metricsAuth: (asString(observability.metricsAuth, "none") === "bearer" ? "bearer" : "none"),
|
|
185
190
|
auditLogPath: resolveConfiguredPath(observability.auditLogPath, path.join(os.homedir(), ".openclaw", "a2a-audit.jsonl"), resolvePath),
|
|
191
|
+
historyEnabled: asBoolean(observability.historyEnabled, true),
|
|
192
|
+
historyLogPath: resolveConfiguredPath(observability.historyLogPath, path.join(os.homedir(), ".openclaw", "a2a-history.jsonl"), resolvePath),
|
|
193
|
+
historyIncludeEncryptedPayloads: asBoolean(observability.historyIncludeEncryptedPayloads, false),
|
|
186
194
|
},
|
|
187
195
|
timeouts: {
|
|
188
196
|
agentResponseTimeoutMs: asNumber(timeouts.agentResponseTimeoutMs, 300_000),
|
|
@@ -212,6 +220,7 @@ export function parseConfig(raw, resolvePath) {
|
|
|
212
220
|
username: asString(registration.username, ""),
|
|
213
221
|
email: asString(registration.email, ""),
|
|
214
222
|
password: asString(registration.password, ""),
|
|
223
|
+
clientId: asString(registration.clientId, ""),
|
|
215
224
|
},
|
|
216
225
|
};
|
|
217
226
|
}
|
|
@@ -221,16 +230,60 @@ function normalizeCardPath() {
|
|
|
221
230
|
}
|
|
222
231
|
return `/${AGENT_CARD_PATH}`;
|
|
223
232
|
}
|
|
224
|
-
function
|
|
225
|
-
if (config.
|
|
226
|
-
|
|
233
|
+
function getAdvertisedAddress(config) {
|
|
234
|
+
if (config.agentCard.url) {
|
|
235
|
+
try {
|
|
236
|
+
const url = new URL(config.agentCard.url);
|
|
237
|
+
return `${url.hostname}:${url.port || (url.protocol === "https:" ? "443" : "80")}`;
|
|
238
|
+
}
|
|
239
|
+
catch {
|
|
240
|
+
// Ignore invalid configured URL and fall back below.
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (config.server.host && config.server.port) {
|
|
244
|
+
return `${config.server.host}:${config.server.port}`;
|
|
245
|
+
}
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
function upsertEphemeralPeer(config, peerName, address, token) {
|
|
249
|
+
const normalizedAddress = address.startsWith("http://") || address.startsWith("https://")
|
|
250
|
+
? address
|
|
251
|
+
: `http://${address}`;
|
|
252
|
+
const agentCardUrl = `${normalizedAddress}/.well-known/agent.json`;
|
|
253
|
+
const existing = config.peers.find((peer) => peer.name === peerName);
|
|
254
|
+
if (existing) {
|
|
255
|
+
existing.agentCardUrl = agentCardUrl;
|
|
256
|
+
existing.auth = { type: "bearer", token };
|
|
257
|
+
return;
|
|
227
258
|
}
|
|
228
|
-
|
|
229
|
-
|
|
259
|
+
config.peers.push({
|
|
260
|
+
name: peerName,
|
|
261
|
+
agentCardUrl,
|
|
262
|
+
auth: { type: "bearer", token },
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
async function waitForHandshakeAnswer(hubClient, matchId, timeoutMs = 45_000) {
|
|
266
|
+
const deadline = Date.now() + timeoutMs;
|
|
267
|
+
while (Date.now() < deadline) {
|
|
268
|
+
const pending = await hubClient.getPendingHandshakeMessages(matchId);
|
|
269
|
+
const answer = pending.find((message) => message.messageType === "answer");
|
|
270
|
+
if (answer) {
|
|
271
|
+
return answer;
|
|
272
|
+
}
|
|
273
|
+
await new Promise((resolve) => setTimeout(resolve, 1_500));
|
|
230
274
|
}
|
|
231
|
-
|
|
275
|
+
throw new Error(`Timed out waiting for handshake answer for match ${matchId}`);
|
|
232
276
|
}
|
|
233
|
-
|
|
277
|
+
function getHandshakeTokenValidationError(token) {
|
|
278
|
+
if (isValidEphemeralInboundToken(token)) {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
const tokenShape = typeof token === "string"
|
|
282
|
+
? `length ${Array.from(token).length}${token.includes("\u2026") ? ", contains U+2026" : ""}`
|
|
283
|
+
: `type ${typeof token}`;
|
|
284
|
+
return `invalid ephemeral handshake token: expected 48 lowercase hex characters, got ${tokenShape}`;
|
|
285
|
+
}
|
|
286
|
+
async function processPendingHubMatches(api, config, processedMessages, historyStore) {
|
|
234
287
|
let hubClient;
|
|
235
288
|
try {
|
|
236
289
|
hubClient = await HubMatchClient.create();
|
|
@@ -239,9 +292,9 @@ async function processPendingHubMatches(api, config, processedMatches) {
|
|
|
239
292
|
api.logger.warn(`claw-crony: pending match polling skipped - ${err instanceof Error ? err.message : String(err)}`);
|
|
240
293
|
return;
|
|
241
294
|
}
|
|
242
|
-
const
|
|
243
|
-
if (!
|
|
244
|
-
api.logger.warn("claw-crony: pending match polling skipped - no
|
|
295
|
+
const identity = loadIdentity();
|
|
296
|
+
if (!identity) {
|
|
297
|
+
api.logger.warn("claw-crony: pending match polling skipped - no local identity available");
|
|
245
298
|
return;
|
|
246
299
|
}
|
|
247
300
|
let matches;
|
|
@@ -257,17 +310,107 @@ async function processPendingHubMatches(api, config, processedMatches) {
|
|
|
257
310
|
continue;
|
|
258
311
|
}
|
|
259
312
|
try {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
313
|
+
const pendingMessages = await hubClient.getPendingHandshakeMessages(match.id);
|
|
314
|
+
for (const message of pendingMessages) {
|
|
315
|
+
if (processedMessages.has(message.id)) {
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
const decrypted = decryptHandshake(message.ciphertext, identity);
|
|
319
|
+
historyStore?.record({
|
|
320
|
+
type: message.messageType === "answer" ? "handshake.answer_received" : "handshake.offer_received",
|
|
321
|
+
status: "started",
|
|
322
|
+
direction: "inbound",
|
|
323
|
+
matchId: match.id,
|
|
324
|
+
messageId: message.id,
|
|
325
|
+
peer: match.requester?.name ?? `agent-${message.senderAgentId}`,
|
|
326
|
+
detail: {
|
|
327
|
+
messageType: message.messageType,
|
|
328
|
+
senderAgentId: message.senderAgentId,
|
|
329
|
+
receiverAgentId: message.receiverAgentId,
|
|
330
|
+
},
|
|
331
|
+
});
|
|
332
|
+
const tokenValidationError = getHandshakeTokenValidationError(decrypted.token);
|
|
333
|
+
if (tokenValidationError) {
|
|
334
|
+
await hubClient.consumeHandshakeMessage(match.id, message.id);
|
|
335
|
+
processedMessages.add(message.id);
|
|
336
|
+
historyStore?.record({
|
|
337
|
+
type: "handshake.failed",
|
|
338
|
+
status: "ignored",
|
|
339
|
+
direction: "inbound",
|
|
340
|
+
matchId: match.id,
|
|
341
|
+
messageId: message.id,
|
|
342
|
+
peer: match.requester?.name ?? `agent-${message.senderAgentId}`,
|
|
343
|
+
detail: { reason: tokenValidationError },
|
|
344
|
+
});
|
|
345
|
+
api.logger.warn(`claw-crony: ignored malformed handshake ${message.id} for match ${match.id} - ${tokenValidationError}`);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
await hubClient.consumeHandshakeMessage(match.id, message.id);
|
|
349
|
+
processedMessages.add(message.id);
|
|
350
|
+
const remoteName = match.callerRole === "provider"
|
|
351
|
+
? (match.requester?.name ?? `agent-${decrypted.fromAgentId}`)
|
|
352
|
+
: (match.provider?.name ?? `agent-${decrypted.fromAgentId}`);
|
|
353
|
+
upsertEphemeralPeer(config, remoteName, decrypted.address, decrypted.token);
|
|
354
|
+
historyStore?.record({
|
|
355
|
+
type: "peer.upserted",
|
|
356
|
+
status: "success",
|
|
357
|
+
direction: "local",
|
|
358
|
+
matchId: match.id,
|
|
359
|
+
peer: remoteName,
|
|
360
|
+
detail: {
|
|
361
|
+
agentCardUrl: config.peers.find((peer) => peer.name === remoteName)?.agentCardUrl,
|
|
362
|
+
tokenExpiresAt: decrypted.tokenExpiresAt,
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
if (message.messageType === "offer" && match.callerRole === "provider") {
|
|
366
|
+
const localAddress = getAdvertisedAddress(config);
|
|
367
|
+
if (!localAddress) {
|
|
368
|
+
api.logger.warn(`claw-crony: cannot answer match ${match.id} because no advertised address is configured`);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const issued = issueEphemeralInboundToken(config, match.id, decrypted.fromAgentId);
|
|
372
|
+
const payload = {
|
|
373
|
+
version: 1,
|
|
374
|
+
matchId: match.id,
|
|
375
|
+
sessionId: crypto.randomUUID(),
|
|
376
|
+
fromAgentId: hubClient.agentId,
|
|
377
|
+
toAgentId: decrypted.fromAgentId,
|
|
378
|
+
address: localAddress,
|
|
379
|
+
agentCardPath: "/.well-known/agent.json",
|
|
380
|
+
token: issued.token,
|
|
381
|
+
tokenExpiresAt: issued.expiresAt,
|
|
382
|
+
protocols: ["jsonrpc", "rest", "grpc"],
|
|
383
|
+
createdAt: new Date().toISOString(),
|
|
384
|
+
nonce: crypto.randomBytes(12).toString("hex"),
|
|
385
|
+
};
|
|
386
|
+
const peerPublicKey = match.requester?.publicKey;
|
|
387
|
+
if (!peerPublicKey) {
|
|
388
|
+
api.logger.warn(`claw-crony: cannot answer match ${match.id} because requester public key is missing`);
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
await hubClient.sendHandshakeMessage(match.id, {
|
|
392
|
+
messageType: "answer",
|
|
393
|
+
ciphertext: encryptHandshake(payload, peerPublicKey),
|
|
394
|
+
expiresAt: issued.expiresAt,
|
|
395
|
+
});
|
|
396
|
+
historyStore?.record({
|
|
397
|
+
type: "handshake.answer_sent",
|
|
398
|
+
status: "success",
|
|
399
|
+
direction: "outbound",
|
|
400
|
+
matchId: match.id,
|
|
401
|
+
peer: remoteName,
|
|
402
|
+
detail: {
|
|
403
|
+
toAgentId: decrypted.fromAgentId,
|
|
404
|
+
expiresAt: issued.expiresAt,
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
await hubClient.markReady(match.id);
|
|
408
|
+
api.logger.info(`claw-crony: answered encrypted handshake for match ${match.id}`);
|
|
409
|
+
}
|
|
410
|
+
else if (message.messageType === "answer") {
|
|
411
|
+
await hubClient.markReady(match.id);
|
|
412
|
+
api.logger.info(`claw-crony: received encrypted handshake answer for match ${match.id}`);
|
|
413
|
+
}
|
|
271
414
|
}
|
|
272
415
|
}
|
|
273
416
|
catch (err) {
|
|
@@ -285,6 +428,10 @@ const plugin = {
|
|
|
285
428
|
structuredLogs: config.observability.structuredLogs,
|
|
286
429
|
});
|
|
287
430
|
const auditLogger = new AuditLogger(config.observability.auditLogPath);
|
|
431
|
+
const historyStore = new RequestHistoryStore(config.observability.historyLogPath, {
|
|
432
|
+
enabled: config.observability.historyEnabled,
|
|
433
|
+
includeEncryptedPayloads: config.observability.historyIncludeEncryptedPayloads,
|
|
434
|
+
});
|
|
288
435
|
const client = new A2AClient();
|
|
289
436
|
const taskStore = new FileTaskStore(config.storage.tasksDir);
|
|
290
437
|
const executor = new QueueingAgentExecutor(new OpenClawAgentExecutor(api, config), telemetry, config.limits);
|
|
@@ -321,6 +468,13 @@ const plugin = {
|
|
|
321
468
|
// Wire audit logger for inbound task completion
|
|
322
469
|
telemetry.setTaskAuditCallback((taskId, contextId, state, durationMs) => {
|
|
323
470
|
auditLogger.recordInbound(taskId, contextId, state, durationMs);
|
|
471
|
+
historyStore.record({
|
|
472
|
+
type: state === "completed" ? "task.inbound_completed" : "task.inbound_failed",
|
|
473
|
+
status: state === "completed" ? "success" : "failure",
|
|
474
|
+
direction: "inbound",
|
|
475
|
+
durationMs,
|
|
476
|
+
detail: { taskId, contextId, state },
|
|
477
|
+
});
|
|
324
478
|
});
|
|
325
479
|
// SDK expects userBuilder(req) -> Promise<User>
|
|
326
480
|
// When bearer auth is configured, validate the Authorization header.
|
|
@@ -397,7 +551,305 @@ const plugin = {
|
|
|
397
551
|
let cleanupTimer = null;
|
|
398
552
|
let hubMatchPollingTimer = null;
|
|
399
553
|
const grpcPort = config.server.port + 1;
|
|
400
|
-
const
|
|
554
|
+
const processedHubMessages = new Set();
|
|
555
|
+
let hubStartupPromise = null;
|
|
556
|
+
let hubShutdownPromise = null;
|
|
557
|
+
const startHubLifecycle = (source) => {
|
|
558
|
+
if (config.hub?.enabled === false) {
|
|
559
|
+
return Promise.resolve();
|
|
560
|
+
}
|
|
561
|
+
if (!hubStartupPromise) {
|
|
562
|
+
hubStartupPromise = (async () => {
|
|
563
|
+
if (config.hub?.registrationEnabled !== false) {
|
|
564
|
+
try {
|
|
565
|
+
const reg = await runHubRegistration(api, config, config.hub, config.registration ?? {});
|
|
566
|
+
if (reg) {
|
|
567
|
+
api.logger.info(`claw-crony: registered with hub (agentId=${reg.agentId}, source=${source})`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
catch (err) {
|
|
571
|
+
api.logger.warn(`claw-crony: hub registration failed - ${err instanceof Error ? err.message : String(err)}`);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
try {
|
|
575
|
+
const hubClient = await HubMatchClient.create();
|
|
576
|
+
await hubClient.updatePresence("online");
|
|
577
|
+
}
|
|
578
|
+
catch (presenceErr) {
|
|
579
|
+
api.logger.warn(`claw-crony: failed to update hub presence - ${presenceErr instanceof Error ? presenceErr.message : String(presenceErr)}`);
|
|
580
|
+
}
|
|
581
|
+
})();
|
|
582
|
+
}
|
|
583
|
+
return hubStartupPromise;
|
|
584
|
+
};
|
|
585
|
+
const stopHubLifecycle = (source) => {
|
|
586
|
+
if (config.hub?.enabled === false) {
|
|
587
|
+
return Promise.resolve();
|
|
588
|
+
}
|
|
589
|
+
if (!hubShutdownPromise) {
|
|
590
|
+
hubShutdownPromise = (async () => {
|
|
591
|
+
try {
|
|
592
|
+
const hubClient = await HubMatchClient.create();
|
|
593
|
+
await hubClient.updatePresence("offline");
|
|
594
|
+
api.logger.info(`claw-crony: hub presence set offline (source=${source})`);
|
|
595
|
+
}
|
|
596
|
+
catch {
|
|
597
|
+
// Ignore best-effort presence shutdown failure.
|
|
598
|
+
}
|
|
599
|
+
})();
|
|
600
|
+
}
|
|
601
|
+
return hubShutdownPromise;
|
|
602
|
+
};
|
|
603
|
+
api.on("gateway_start", async () => {
|
|
604
|
+
await startHubLifecycle("gateway_start");
|
|
605
|
+
}, { priority: 50 });
|
|
606
|
+
api.on("gateway_stop", async () => {
|
|
607
|
+
await stopHubLifecycle("gateway_stop");
|
|
608
|
+
}, { priority: 50 });
|
|
609
|
+
const performMatchRequest = async (input) => {
|
|
610
|
+
const startedAt = Date.now();
|
|
611
|
+
let hubClient;
|
|
612
|
+
try {
|
|
613
|
+
hubClient = await HubMatchClient.create();
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
617
|
+
historyStore.record({
|
|
618
|
+
type: "match.failed",
|
|
619
|
+
status: "failure",
|
|
620
|
+
direction: "outbound",
|
|
621
|
+
durationMs: Date.now() - startedAt,
|
|
622
|
+
detail: { reason: msg },
|
|
623
|
+
});
|
|
624
|
+
return {
|
|
625
|
+
ok: false,
|
|
626
|
+
text: `Not registered with hub: ${msg}`,
|
|
627
|
+
details: { ok: false, error: msg },
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
const identity = loadIdentity();
|
|
631
|
+
if (!identity) {
|
|
632
|
+
historyStore.record({
|
|
633
|
+
type: "match.failed",
|
|
634
|
+
status: "failure",
|
|
635
|
+
direction: "outbound",
|
|
636
|
+
durationMs: Date.now() - startedAt,
|
|
637
|
+
detail: { reason: "identity_missing" },
|
|
638
|
+
});
|
|
639
|
+
return {
|
|
640
|
+
ok: false,
|
|
641
|
+
text: "No local identity found. Restart the plugin and try again.",
|
|
642
|
+
details: { ok: false, error: "identity_missing" },
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
const localAddress = getAdvertisedAddress(config);
|
|
646
|
+
if (!localAddress) {
|
|
647
|
+
historyStore.record({
|
|
648
|
+
type: "match.failed",
|
|
649
|
+
status: "failure",
|
|
650
|
+
direction: "outbound",
|
|
651
|
+
durationMs: Date.now() - startedAt,
|
|
652
|
+
detail: { reason: "address_missing" },
|
|
653
|
+
});
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
text: "No advertised address is configured for this agent.",
|
|
657
|
+
details: { ok: false, error: "address_missing" },
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
let match;
|
|
661
|
+
try {
|
|
662
|
+
match = await hubClient.createMatch({
|
|
663
|
+
skills: input.skills,
|
|
664
|
+
description: input.description,
|
|
665
|
+
});
|
|
666
|
+
historyStore.record({
|
|
667
|
+
type: "match.created",
|
|
668
|
+
status: "success",
|
|
669
|
+
direction: "outbound",
|
|
670
|
+
matchId: match.id,
|
|
671
|
+
peer: match.provider?.name,
|
|
672
|
+
detail: {
|
|
673
|
+
skills: input.skills,
|
|
674
|
+
description: input.description,
|
|
675
|
+
providerAgentId: match.provider?.id,
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
catch (err) {
|
|
680
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
681
|
+
historyStore.record({
|
|
682
|
+
type: "match.failed",
|
|
683
|
+
status: "failure",
|
|
684
|
+
direction: "outbound",
|
|
685
|
+
durationMs: Date.now() - startedAt,
|
|
686
|
+
detail: { reason: msg, skills: input.skills },
|
|
687
|
+
});
|
|
688
|
+
return {
|
|
689
|
+
ok: false,
|
|
690
|
+
text: `Failed to create match: ${msg}`,
|
|
691
|
+
details: { ok: false, error: msg },
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
const provider = match.provider;
|
|
695
|
+
const providerPublicKey = provider?.publicKey;
|
|
696
|
+
if (!provider || !providerPublicKey) {
|
|
697
|
+
historyStore.record({
|
|
698
|
+
type: "handshake.failed",
|
|
699
|
+
status: "failure",
|
|
700
|
+
direction: "outbound",
|
|
701
|
+
matchId: match.id,
|
|
702
|
+
peer: provider?.name,
|
|
703
|
+
detail: { reason: "provider_public_key_missing" },
|
|
704
|
+
});
|
|
705
|
+
return {
|
|
706
|
+
ok: false,
|
|
707
|
+
text: `Match created (id=${match.id}) but provider public key is missing`,
|
|
708
|
+
details: { ok: false, matchId: match.id, error: "provider_public_key_missing" },
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const issued = issueEphemeralInboundToken(config, match.id, provider.id);
|
|
712
|
+
const localPayload = {
|
|
713
|
+
version: 1,
|
|
714
|
+
matchId: match.id,
|
|
715
|
+
sessionId: crypto.randomUUID(),
|
|
716
|
+
fromAgentId: hubClient.agentId,
|
|
717
|
+
toAgentId: provider.id,
|
|
718
|
+
address: localAddress,
|
|
719
|
+
agentCardPath: "/.well-known/agent.json",
|
|
720
|
+
token: issued.token,
|
|
721
|
+
tokenExpiresAt: issued.expiresAt,
|
|
722
|
+
protocols: ["jsonrpc", "rest", "grpc"],
|
|
723
|
+
createdAt: new Date().toISOString(),
|
|
724
|
+
nonce: crypto.randomBytes(12).toString("hex"),
|
|
725
|
+
};
|
|
726
|
+
try {
|
|
727
|
+
await hubClient.sendHandshakeMessage(match.id, {
|
|
728
|
+
messageType: "offer",
|
|
729
|
+
ciphertext: encryptHandshake(localPayload, providerPublicKey),
|
|
730
|
+
expiresAt: issued.expiresAt,
|
|
731
|
+
});
|
|
732
|
+
historyStore.record({
|
|
733
|
+
type: "handshake.offer_sent",
|
|
734
|
+
status: "success",
|
|
735
|
+
direction: "outbound",
|
|
736
|
+
matchId: match.id,
|
|
737
|
+
peer: provider.name,
|
|
738
|
+
detail: {
|
|
739
|
+
toAgentId: provider.id,
|
|
740
|
+
address: localAddress,
|
|
741
|
+
expiresAt: issued.expiresAt,
|
|
742
|
+
},
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
catch (err) {
|
|
746
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
747
|
+
historyStore.record({
|
|
748
|
+
type: "handshake.failed",
|
|
749
|
+
status: "failure",
|
|
750
|
+
direction: "outbound",
|
|
751
|
+
matchId: match.id,
|
|
752
|
+
peer: provider.name,
|
|
753
|
+
durationMs: Date.now() - startedAt,
|
|
754
|
+
detail: { reason: msg, phase: "offer" },
|
|
755
|
+
});
|
|
756
|
+
return {
|
|
757
|
+
ok: false,
|
|
758
|
+
text: `Match created (id=${match.id}) but failed to send encrypted handshake offer: ${msg}`,
|
|
759
|
+
details: { ok: false, matchId: match.id, error: msg },
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
let answer;
|
|
763
|
+
try {
|
|
764
|
+
answer = await waitForHandshakeAnswer(hubClient, match.id);
|
|
765
|
+
}
|
|
766
|
+
catch (err) {
|
|
767
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
768
|
+
historyStore.record({
|
|
769
|
+
type: "handshake.failed",
|
|
770
|
+
status: "failure",
|
|
771
|
+
direction: "inbound",
|
|
772
|
+
matchId: match.id,
|
|
773
|
+
peer: provider.name,
|
|
774
|
+
durationMs: Date.now() - startedAt,
|
|
775
|
+
detail: { reason: msg, phase: "wait_answer" },
|
|
776
|
+
});
|
|
777
|
+
return {
|
|
778
|
+
ok: false,
|
|
779
|
+
text: `Encrypted handshake offer sent for match ${match.id}, but waiting for answer failed: ${msg}`,
|
|
780
|
+
details: { ok: false, matchId: match.id, error: msg },
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
let remotePayload;
|
|
784
|
+
try {
|
|
785
|
+
remotePayload = decryptHandshake(answer.ciphertext, identity);
|
|
786
|
+
const tokenValidationError = getHandshakeTokenValidationError(remotePayload.token);
|
|
787
|
+
if (tokenValidationError) {
|
|
788
|
+
throw new Error(tokenValidationError);
|
|
789
|
+
}
|
|
790
|
+
await hubClient.consumeHandshakeMessage(match.id, answer.id);
|
|
791
|
+
historyStore.record({
|
|
792
|
+
type: "handshake.answer_received",
|
|
793
|
+
status: "success",
|
|
794
|
+
direction: "inbound",
|
|
795
|
+
matchId: match.id,
|
|
796
|
+
messageId: answer.id,
|
|
797
|
+
peer: provider.name,
|
|
798
|
+
detail: {
|
|
799
|
+
fromAgentId: answer.senderAgentId,
|
|
800
|
+
address: remotePayload.address,
|
|
801
|
+
tokenExpiresAt: remotePayload.tokenExpiresAt,
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
upsertEphemeralPeer(config, provider.name, remotePayload.address, remotePayload.token);
|
|
805
|
+
historyStore.record({
|
|
806
|
+
type: "peer.upserted",
|
|
807
|
+
status: "success",
|
|
808
|
+
direction: "local",
|
|
809
|
+
matchId: match.id,
|
|
810
|
+
peer: provider.name,
|
|
811
|
+
detail: {
|
|
812
|
+
agentCardUrl: config.peers.find((peer) => peer.name === provider.name)?.agentCardUrl,
|
|
813
|
+
tokenExpiresAt: remotePayload.tokenExpiresAt,
|
|
814
|
+
},
|
|
815
|
+
});
|
|
816
|
+
await hubClient.markReady(match.id);
|
|
817
|
+
}
|
|
818
|
+
catch (err) {
|
|
819
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
820
|
+
historyStore.record({
|
|
821
|
+
type: "handshake.failed",
|
|
822
|
+
status: "failure",
|
|
823
|
+
direction: "inbound",
|
|
824
|
+
matchId: match.id,
|
|
825
|
+
messageId: answer.id,
|
|
826
|
+
peer: provider.name,
|
|
827
|
+
durationMs: Date.now() - startedAt,
|
|
828
|
+
detail: { reason: msg, phase: "process_answer" },
|
|
829
|
+
});
|
|
830
|
+
return {
|
|
831
|
+
ok: false,
|
|
832
|
+
text: `Encrypted handshake answer received for match ${match.id}, but processing failed: ${msg}`,
|
|
833
|
+
details: { ok: false, matchId: match.id, error: msg },
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
ok: true,
|
|
838
|
+
text: `Encrypted handshake ready: match=${match.id}\n` +
|
|
839
|
+
`Provider: ${provider.name}\n` +
|
|
840
|
+
`Address: ${remotePayload.address}\n` +
|
|
841
|
+
`Temporary token: ${remotePayload.token}\n` +
|
|
842
|
+
`Token expires at: ${remotePayload.tokenExpiresAt}`,
|
|
843
|
+
details: {
|
|
844
|
+
ok: true,
|
|
845
|
+
matchId: match.id,
|
|
846
|
+
status: "ready_to_connect",
|
|
847
|
+
providerAddress: remotePayload.address,
|
|
848
|
+
peerToken: remotePayload.token,
|
|
849
|
+
tokenExpiresAt: remotePayload.tokenExpiresAt,
|
|
850
|
+
},
|
|
851
|
+
};
|
|
852
|
+
};
|
|
401
853
|
api.registerGatewayMethod("a2a.metrics", ({ respond }) => {
|
|
402
854
|
respond(true, {
|
|
403
855
|
metrics: telemetry.snapshot(),
|
|
@@ -411,6 +863,49 @@ const plugin = {
|
|
|
411
863
|
.then((entries) => respond(true, { entries, count: entries.length }))
|
|
412
864
|
.catch((error) => respond(false, { error: String(error?.message || error) }));
|
|
413
865
|
});
|
|
866
|
+
api.registerGatewayMethod("a2a.history", ({ params, respond }) => {
|
|
867
|
+
const payload = asObject(params);
|
|
868
|
+
const matchId = asNumber(payload.matchId, Number.NaN);
|
|
869
|
+
historyStore
|
|
870
|
+
.tail({
|
|
871
|
+
count: Math.min(Math.max(1, asNumber(payload.count ?? payload.limit, 50)), 500),
|
|
872
|
+
type: asString(payload.type, ""),
|
|
873
|
+
status: asString(payload.status, ""),
|
|
874
|
+
direction: asString(payload.direction, ""),
|
|
875
|
+
matchId: Number.isFinite(matchId) ? matchId : undefined,
|
|
876
|
+
peer: asString(payload.peer, ""),
|
|
877
|
+
})
|
|
878
|
+
.then((entries) => respond(true, { entries, count: entries.length }))
|
|
879
|
+
.catch((error) => respond(false, { error: String(error?.message || error) }));
|
|
880
|
+
});
|
|
881
|
+
api.registerGatewayMethod("a2a.peers", ({ respond }) => {
|
|
882
|
+
const peerStates = healthManager?.getAllStates() ?? new Map();
|
|
883
|
+
respond(true, {
|
|
884
|
+
peers: config.peers.map((peer) => ({
|
|
885
|
+
name: peer.name,
|
|
886
|
+
agentCardUrl: peer.agentCardUrl,
|
|
887
|
+
authType: peer.auth?.type,
|
|
888
|
+
hasToken: Boolean(peer.auth?.token),
|
|
889
|
+
health: peerStates.get(peer.name) ?? null,
|
|
890
|
+
})),
|
|
891
|
+
});
|
|
892
|
+
});
|
|
893
|
+
api.registerGatewayMethod("a2a.match", ({ params, respond }) => {
|
|
894
|
+
const payload = asObject(params);
|
|
895
|
+
const skills = Array.isArray(payload.skills)
|
|
896
|
+
? payload.skills.filter((entry) => typeof entry === "string" && entry.trim().length > 0)
|
|
897
|
+
: [];
|
|
898
|
+
if (skills.length === 0) {
|
|
899
|
+
respond(false, { error: "skills must be a non-empty string array" });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
performMatchRequest({
|
|
903
|
+
skills,
|
|
904
|
+
description: asString(payload.description, ""),
|
|
905
|
+
})
|
|
906
|
+
.then((result) => respond(result.ok, result.details))
|
|
907
|
+
.catch((error) => respond(false, { error: String(error?.message || error) }));
|
|
908
|
+
});
|
|
414
909
|
api.registerGatewayMethod("a2a.send", ({ params, respond }) => {
|
|
415
910
|
const payload = asObject(params);
|
|
416
911
|
const peerName = asString(payload.peer || payload.name, "");
|
|
@@ -439,6 +934,16 @@ const plugin = {
|
|
|
439
934
|
message.agentId = resolvedAgentId;
|
|
440
935
|
}
|
|
441
936
|
const startedAt = Date.now();
|
|
937
|
+
historyStore.record({
|
|
938
|
+
type: "send.started",
|
|
939
|
+
status: "started",
|
|
940
|
+
direction: "outbound",
|
|
941
|
+
peer: peer.name,
|
|
942
|
+
detail: {
|
|
943
|
+
resolvedAgentId: message.agentId,
|
|
944
|
+
hasParts: Array.isArray(message.parts),
|
|
945
|
+
},
|
|
946
|
+
});
|
|
442
947
|
const sendOptions = {
|
|
443
948
|
healthManager: healthManager ?? undefined,
|
|
444
949
|
retryConfig: config.resilience.retry,
|
|
@@ -455,6 +960,17 @@ const plugin = {
|
|
|
455
960
|
const outDuration = Date.now() - startedAt;
|
|
456
961
|
telemetry.recordOutboundRequest(peer.name, result.ok, result.statusCode, outDuration);
|
|
457
962
|
auditLogger.recordOutbound(peer.name, result.ok, result.statusCode, outDuration);
|
|
963
|
+
historyStore.record({
|
|
964
|
+
type: result.ok ? "send.completed" : "send.failed",
|
|
965
|
+
status: result.ok ? "success" : "failure",
|
|
966
|
+
direction: "outbound",
|
|
967
|
+
peer: peer.name,
|
|
968
|
+
durationMs: outDuration,
|
|
969
|
+
detail: {
|
|
970
|
+
statusCode: result.statusCode,
|
|
971
|
+
response: result.ok ? undefined : result.response,
|
|
972
|
+
},
|
|
973
|
+
});
|
|
458
974
|
if (result.ok) {
|
|
459
975
|
respond(true, {
|
|
460
976
|
statusCode: result.statusCode,
|
|
@@ -471,6 +987,14 @@ const plugin = {
|
|
|
471
987
|
const errDuration = Date.now() - startedAt;
|
|
472
988
|
telemetry.recordOutboundRequest(peer.name, false, 500, errDuration);
|
|
473
989
|
auditLogger.recordOutbound(peer.name, false, 500, errDuration);
|
|
990
|
+
historyStore.record({
|
|
991
|
+
type: "send.failed",
|
|
992
|
+
status: "failure",
|
|
993
|
+
direction: "outbound",
|
|
994
|
+
peer: peer.name,
|
|
995
|
+
durationMs: errDuration,
|
|
996
|
+
detail: { error: String(error?.message || error) },
|
|
997
|
+
});
|
|
474
998
|
respond(false, { error: String(error?.message || error) });
|
|
475
999
|
});
|
|
476
1000
|
});
|
|
@@ -498,64 +1022,103 @@ const plugin = {
|
|
|
498
1022
|
label: "A2A Send File",
|
|
499
1023
|
parameters: sendFileParams,
|
|
500
1024
|
async execute(toolCallId, params) {
|
|
501
|
-
const
|
|
1025
|
+
const input = params;
|
|
1026
|
+
const peer = config.peers.find((p) => p.name === input.peer);
|
|
502
1027
|
if (!peer) {
|
|
503
1028
|
const available = config.peers.map((p) => p.name).join(", ") || "(none)";
|
|
504
1029
|
return {
|
|
505
|
-
content: [{ type: "text", text: `Peer not found: "${
|
|
1030
|
+
content: [{ type: "text", text: `Peer not found: "${input.peer}". Available peers: ${available}` }],
|
|
506
1031
|
details: { ok: false },
|
|
507
1032
|
};
|
|
508
1033
|
}
|
|
509
1034
|
// Security checks: SSRF, MIME, file size
|
|
510
|
-
const uriCheck = await validateUri(
|
|
1035
|
+
const uriCheck = await validateUri(input.uri, config.security);
|
|
511
1036
|
if (!uriCheck.ok) {
|
|
512
1037
|
return {
|
|
513
1038
|
content: [{ type: "text", text: `URI rejected: ${uriCheck.reason}` }],
|
|
514
1039
|
details: { ok: false, reason: uriCheck.reason },
|
|
515
1040
|
};
|
|
516
1041
|
}
|
|
517
|
-
if (
|
|
1042
|
+
if (input.mimeType && !validateMimeType(input.mimeType, config.security.allowedMimeTypes)) {
|
|
518
1043
|
return {
|
|
519
|
-
content: [{ type: "text", text: `MIME type rejected: "${
|
|
1044
|
+
content: [{ type: "text", text: `MIME type rejected: "${input.mimeType}" is not in the allowed list` }],
|
|
520
1045
|
details: { ok: false },
|
|
521
1046
|
};
|
|
522
1047
|
}
|
|
523
1048
|
const parts = [];
|
|
524
|
-
if (
|
|
525
|
-
parts.push({ kind: "text", text:
|
|
1049
|
+
if (input.text) {
|
|
1050
|
+
parts.push({ kind: "text", text: input.text });
|
|
526
1051
|
}
|
|
527
1052
|
parts.push({
|
|
528
1053
|
kind: "file",
|
|
529
1054
|
file: {
|
|
530
|
-
uri:
|
|
531
|
-
...(
|
|
532
|
-
...(
|
|
1055
|
+
uri: input.uri,
|
|
1056
|
+
...(input.name ? { name: input.name } : {}),
|
|
1057
|
+
...(input.mimeType ? { mimeType: input.mimeType } : {}),
|
|
533
1058
|
},
|
|
534
1059
|
});
|
|
535
1060
|
try {
|
|
536
1061
|
const message = { parts };
|
|
537
|
-
if (
|
|
538
|
-
message.agentId =
|
|
1062
|
+
if (input.agentId) {
|
|
1063
|
+
message.agentId = input.agentId;
|
|
539
1064
|
}
|
|
1065
|
+
const startedAt = Date.now();
|
|
1066
|
+
historyStore.record({
|
|
1067
|
+
type: "send_file.started",
|
|
1068
|
+
status: "started",
|
|
1069
|
+
direction: "outbound",
|
|
1070
|
+
peer: peer.name,
|
|
1071
|
+
detail: {
|
|
1072
|
+
uri: input.uri,
|
|
1073
|
+
name: input.name,
|
|
1074
|
+
mimeType: input.mimeType,
|
|
1075
|
+
agentId: input.agentId,
|
|
1076
|
+
},
|
|
1077
|
+
});
|
|
540
1078
|
const result = await client.sendMessage(peer, message, {
|
|
541
1079
|
healthManager: healthManager ?? undefined,
|
|
542
1080
|
retryConfig: config.resilience.retry,
|
|
543
1081
|
});
|
|
1082
|
+
historyStore.record({
|
|
1083
|
+
type: result.ok ? "send_file.completed" : "send_file.failed",
|
|
1084
|
+
status: result.ok ? "success" : "failure",
|
|
1085
|
+
direction: "outbound",
|
|
1086
|
+
peer: peer.name,
|
|
1087
|
+
durationMs: Date.now() - startedAt,
|
|
1088
|
+
detail: {
|
|
1089
|
+
uri: input.uri,
|
|
1090
|
+
name: input.name,
|
|
1091
|
+
mimeType: input.mimeType,
|
|
1092
|
+
response: result.ok ? undefined : result.response,
|
|
1093
|
+
},
|
|
1094
|
+
});
|
|
544
1095
|
if (result.ok) {
|
|
545
1096
|
return {
|
|
546
|
-
content: [{ type: "text", text: `File sent to ${
|
|
1097
|
+
content: [{ type: "text", text: `File sent to ${input.peer} via A2A.\nURI: ${input.uri}\nResponse: ${JSON.stringify(result.response)}` }],
|
|
547
1098
|
details: { ok: true, response: result.response },
|
|
548
1099
|
};
|
|
549
1100
|
}
|
|
550
1101
|
return {
|
|
551
|
-
content: [{ type: "text", text: `Failed to send file to ${
|
|
1102
|
+
content: [{ type: "text", text: `Failed to send file to ${input.peer}: ${JSON.stringify(result.response)}` }],
|
|
552
1103
|
details: { ok: false, response: result.response },
|
|
553
1104
|
};
|
|
554
1105
|
}
|
|
555
1106
|
catch (err) {
|
|
556
1107
|
const msg = err instanceof Error ? err.message : String(err);
|
|
1108
|
+
historyStore.record({
|
|
1109
|
+
type: "send_file.failed",
|
|
1110
|
+
status: "failure",
|
|
1111
|
+
direction: "outbound",
|
|
1112
|
+
peer: peer.name,
|
|
1113
|
+
detail: {
|
|
1114
|
+
uri: input.uri,
|
|
1115
|
+
name: input.name,
|
|
1116
|
+
mimeType: input.mimeType,
|
|
1117
|
+
error: msg,
|
|
1118
|
+
},
|
|
1119
|
+
});
|
|
557
1120
|
return {
|
|
558
|
-
content: [{ type: "text", text: `Error sending file to ${
|
|
1121
|
+
content: [{ type: "text", text: `Error sending file to ${input.peer}: ${msg}` }],
|
|
559
1122
|
details: { ok: false, error: msg },
|
|
560
1123
|
};
|
|
561
1124
|
}
|
|
@@ -563,14 +1126,13 @@ const plugin = {
|
|
|
563
1126
|
});
|
|
564
1127
|
// ------------------------------------------------------------------
|
|
565
1128
|
// Agent tool: a2a_match_request
|
|
566
|
-
// Creates a match request on the hub and
|
|
567
|
-
// Returns provider address + yourToken + peerToken for A2A communication.
|
|
1129
|
+
// Creates a match request on the hub and performs encrypted connection-info handshake.
|
|
568
1130
|
// ------------------------------------------------------------------
|
|
569
1131
|
api.registerTool({
|
|
570
1132
|
name: "a2a_match_request",
|
|
571
1133
|
description: "Request a match with another agent via the hub. " +
|
|
572
1134
|
"The hub finds a provider agent with matching skills, creates a match record, " +
|
|
573
|
-
"and
|
|
1135
|
+
"and relays encrypted handshake messages so both agents can exchange temporary A2A connection details. " +
|
|
574
1136
|
"Use this to discover and connect with peer agents through the hub's registry.",
|
|
575
1137
|
label: "A2A Match Request",
|
|
576
1138
|
parameters: {
|
|
@@ -589,65 +1151,11 @@ const plugin = {
|
|
|
589
1151
|
},
|
|
590
1152
|
},
|
|
591
1153
|
async execute(toolCallId, params) {
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
client = await HubMatchClient.create();
|
|
595
|
-
}
|
|
596
|
-
catch (err) {
|
|
597
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
598
|
-
return {
|
|
599
|
-
content: [{ type: "text", text: `Not registered with hub: ${msg}` }],
|
|
600
|
-
details: { ok: false, error: msg },
|
|
601
|
-
};
|
|
602
|
-
}
|
|
603
|
-
let match;
|
|
604
|
-
try {
|
|
605
|
-
match = await client.createMatch({
|
|
606
|
-
skills: params.skills,
|
|
607
|
-
description: params.description,
|
|
608
|
-
token: getAdvertisedInboundToken(config, client) ?? client.registrationToken,
|
|
609
|
-
});
|
|
610
|
-
}
|
|
611
|
-
catch (err) {
|
|
612
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
613
|
-
return {
|
|
614
|
-
content: [{ type: "text", text: `Failed to create match: ${msg}` }],
|
|
615
|
-
details: { ok: false, error: msg },
|
|
616
|
-
};
|
|
617
|
-
}
|
|
618
|
-
// Submit our token
|
|
619
|
-
let updatedMatch;
|
|
620
|
-
try {
|
|
621
|
-
updatedMatch = await client.submitToken(match.id, getAdvertisedInboundToken(config, client) ?? client.registrationToken);
|
|
622
|
-
}
|
|
623
|
-
catch (err) {
|
|
624
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
625
|
-
return {
|
|
626
|
-
content: [{ type: "text", text: `Match created (id=${match.id}) but failed to submit token: ${msg}` }],
|
|
627
|
-
details: { ok: false, matchId: match.id, error: msg },
|
|
628
|
-
};
|
|
629
|
-
}
|
|
630
|
-
const provider = updatedMatch.provider;
|
|
631
|
-
const providerAddress = provider?.address ?? "(unknown)";
|
|
632
|
-
const providerAccessToken = updatedMatch.yourToken ?? "(none)";
|
|
633
|
-
const requesterInboundToken = updatedMatch.peerToken ?? "(none)";
|
|
634
|
-
const status = updatedMatch.status;
|
|
1154
|
+
const input = params;
|
|
1155
|
+
const result = await performMatchRequest(input);
|
|
635
1156
|
return {
|
|
636
|
-
content: [{
|
|
637
|
-
|
|
638
|
-
text: `Match ${status}: id=${updatedMatch.id}\n` +
|
|
639
|
-
`Provider: ${provider?.name ?? "(unknown)"} at ${providerAddress}\n` +
|
|
640
|
-
`Provider access token (use to contact provider): ${providerAccessToken}\n` +
|
|
641
|
-
`Requester inbound token (provider uses this to contact you): ${requesterInboundToken}`,
|
|
642
|
-
}],
|
|
643
|
-
details: {
|
|
644
|
-
ok: true,
|
|
645
|
-
matchId: updatedMatch.id,
|
|
646
|
-
status: updatedMatch.status,
|
|
647
|
-
providerAddress,
|
|
648
|
-
yourToken: providerAccessToken,
|
|
649
|
-
peerToken: requesterInboundToken,
|
|
650
|
-
},
|
|
1157
|
+
content: [{ type: "text", text: result.text }],
|
|
1158
|
+
details: result.details,
|
|
651
1159
|
};
|
|
652
1160
|
},
|
|
653
1161
|
});
|
|
@@ -662,19 +1170,7 @@ const plugin = {
|
|
|
662
1170
|
if (server) {
|
|
663
1171
|
return;
|
|
664
1172
|
}
|
|
665
|
-
|
|
666
|
-
if (config.hub?.enabled !== false && config.hub?.registrationEnabled !== false) {
|
|
667
|
-
try {
|
|
668
|
-
const reg = await runHubRegistration(api, config, config.hub, config.registration ?? {});
|
|
669
|
-
if (reg) {
|
|
670
|
-
api.logger.info(`claw-crony: registered with hub (agentId=${reg.agentId})`);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
catch (err) {
|
|
674
|
-
api.logger.warn(`claw-crony: hub registration failed — ${err instanceof Error ? err.message : String(err)}`);
|
|
675
|
-
// Continue startup anyway — hub is optional
|
|
676
|
-
}
|
|
677
|
-
}
|
|
1173
|
+
await startHubLifecycle("service");
|
|
678
1174
|
// Start peer health checks
|
|
679
1175
|
healthManager?.start();
|
|
680
1176
|
// Start HTTP server (JSON-RPC + REST)
|
|
@@ -742,7 +1238,7 @@ const plugin = {
|
|
|
742
1238
|
if (config.hub?.enabled !== false) {
|
|
743
1239
|
const pollingIntervalMs = Math.max(5_000, config.resilience.healthCheck.intervalMs);
|
|
744
1240
|
const pollHubMatches = () => {
|
|
745
|
-
void processPendingHubMatches(api, config,
|
|
1241
|
+
void processPendingHubMatches(api, config, processedHubMessages, historyStore);
|
|
746
1242
|
};
|
|
747
1243
|
pollHubMatches();
|
|
748
1244
|
hubMatchPollingTimer = setInterval(pollHubMatches, pollingIntervalMs);
|
|
@@ -754,6 +1250,8 @@ const plugin = {
|
|
|
754
1250
|
// Stop peer health checks
|
|
755
1251
|
healthManager?.stop();
|
|
756
1252
|
auditLogger.close();
|
|
1253
|
+
historyStore.close();
|
|
1254
|
+
await stopHubLifecycle("service");
|
|
757
1255
|
// Stop task cleanup timer
|
|
758
1256
|
if (cleanupTimer) {
|
|
759
1257
|
clearInterval(cleanupTimer);
|