@clawcrony/claw-crony 1.1.0 → 1.2.2
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/README.md +18 -10
- package/dist/index.js +79 -9
- package/dist/src/hub-match.d.ts +7 -0
- package/dist/src/hub-match.js +6 -0
- package/dist/src/skill-catalog.d.ts +4 -0
- package/dist/src/skill-catalog.js +53 -0
- package/openclaw.plugin.json +8 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,13 +15,21 @@ OpenClaw A2A v0.3.0 Gateway — Auto-discovery and secure communication between
|
|
|
15
15
|
- **File Transfer** — URI / base64 / MIME whitelist + SSRF protection
|
|
16
16
|
- **Observability** — JSONL audit logs + Telemetry metrics endpoint
|
|
17
17
|
|
|
18
|
-
## Hub Server
|
|
19
|
-
|
|
20
|
-
Default Hub: `https://www.factormining.cn`
|
|
21
|
-
|
|
22
|
-
After installation, the plugin auto-registers with the Hub (requires `registrationEnabled: true`). Once registered, use the `a2a_match_request` tool to
|
|
23
|
-
|
|
24
|
-
|
|
18
|
+
## Hub Server
|
|
19
|
+
|
|
20
|
+
Default Hub: `https://www.factormining.cn`
|
|
21
|
+
|
|
22
|
+
After installation, the plugin auto-registers with the Hub (requires `registrationEnabled: true`). Once registered, use the `a2a_match_request` tool to send a matchmaking request, and the Hub will return a matched peer Agent address together with the tokens needed for the current session.
|
|
23
|
+
|
|
24
|
+
After the user signs in to the Hub web dashboard, they can currently see:
|
|
25
|
+
|
|
26
|
+
- Their own Agent profile, address, and normalized skill tags
|
|
27
|
+
- A match timeline for requests created by this Agent
|
|
28
|
+
- Per-request request summary, required skills, and current status
|
|
29
|
+
- Matched result details including provider name, provider address, and update time
|
|
30
|
+
- Whether requester/provider tokens have already been submitted for the match
|
|
31
|
+
|
|
32
|
+
A2A service port: **18800** (default)
|
|
25
33
|
|
|
26
34
|
## Installation
|
|
27
35
|
|
|
@@ -55,9 +63,9 @@ openclaw config set plugins.entries.claw-crony.config.peers '[{
|
|
|
55
63
|
openclaw gateway restart
|
|
56
64
|
```
|
|
57
65
|
|
|
58
|
-
## Hub Matchmaking (a2a_match_request)
|
|
59
|
-
|
|
60
|
-
|
|
66
|
+
## Hub Matchmaking (a2a_match_request)
|
|
67
|
+
|
|
68
|
+
Send a matchmaking request to the Hub, which automatically finds registered Agents with the required skills:
|
|
61
69
|
|
|
62
70
|
```bash
|
|
63
71
|
# Agent calls a2a_match_request tool with params:
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,7 @@ import { AuditLogger } from "./src/audit.js";
|
|
|
24
24
|
import { PeerHealthManager } from "./src/peer-health.js";
|
|
25
25
|
import { runHubRegistration } from "./src/hub-registration.js";
|
|
26
26
|
import { HubMatchClient } from "./src/hub-match.js";
|
|
27
|
+
import { normalizeAgentCardSkills } from "./src/skill-catalog.js";
|
|
27
28
|
import { parseRoutingRules, matchRule } from "./src/routing-rules.js";
|
|
28
29
|
import { validateUri, validateMimeType, } from "./src/file-security.js";
|
|
29
30
|
/** Build a JSON-RPC error response. */
|
|
@@ -71,7 +72,7 @@ function extractSkillsFromAgentCard(card) {
|
|
|
71
72
|
return skills.map((s) => (typeof s === "string" ? s : asObject(s).name ?? "")).filter(Boolean);
|
|
72
73
|
}
|
|
73
74
|
function parseAgentCard(raw) {
|
|
74
|
-
const skills = Array.isArray(raw.skills) ? raw.skills : [];
|
|
75
|
+
const skills = normalizeAgentCardSkills(Array.isArray(raw.skills) ? raw.skills : []);
|
|
75
76
|
return {
|
|
76
77
|
name: asString(raw.name, "OpenClaw A2A Gateway"),
|
|
77
78
|
description: asString(raw.description, "A2A bridge for OpenClaw agents"),
|
|
@@ -220,6 +221,60 @@ function normalizeCardPath() {
|
|
|
220
221
|
}
|
|
221
222
|
return `/${AGENT_CARD_PATH}`;
|
|
222
223
|
}
|
|
224
|
+
function getAdvertisedInboundToken(config, hubClient) {
|
|
225
|
+
if (config.security.tokens && config.security.tokens.length > 0) {
|
|
226
|
+
return config.security.tokens[0] ?? null;
|
|
227
|
+
}
|
|
228
|
+
if (config.security.token) {
|
|
229
|
+
return config.security.token;
|
|
230
|
+
}
|
|
231
|
+
return hubClient?.registrationToken ?? null;
|
|
232
|
+
}
|
|
233
|
+
async function processPendingHubMatches(api, config, processedMatches) {
|
|
234
|
+
let hubClient;
|
|
235
|
+
try {
|
|
236
|
+
hubClient = await HubMatchClient.create();
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
api.logger.warn(`claw-crony: pending match polling skipped - ${err instanceof Error ? err.message : String(err)}`);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const inboundToken = getAdvertisedInboundToken(config, hubClient);
|
|
243
|
+
if (!inboundToken) {
|
|
244
|
+
api.logger.warn("claw-crony: pending match polling skipped - no inbound token available");
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
let matches;
|
|
248
|
+
try {
|
|
249
|
+
matches = await hubClient.getPendingMatches();
|
|
250
|
+
}
|
|
251
|
+
catch (err) {
|
|
252
|
+
api.logger.warn(`claw-crony: failed to fetch pending matches - ${err instanceof Error ? err.message : String(err)}`);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
for (const match of matches) {
|
|
256
|
+
if (match.callerRole !== "provider") {
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const alreadySubmitted = match.providerTokenSubmitted === true || processedMatches.has(match.id);
|
|
261
|
+
let currentMatch = match;
|
|
262
|
+
if (!alreadySubmitted) {
|
|
263
|
+
currentMatch = await hubClient.submitToken(match.id, inboundToken);
|
|
264
|
+
processedMatches.add(match.id);
|
|
265
|
+
api.logger.info(`claw-crony: submitted provider token for hub match ${match.id}`);
|
|
266
|
+
}
|
|
267
|
+
if (currentMatch.readyForComplete === true && currentMatch.status === "token_exchange") {
|
|
268
|
+
await hubClient.completeMatch(match.id, inboundToken);
|
|
269
|
+
processedMatches.delete(match.id);
|
|
270
|
+
api.logger.info(`claw-crony: completed hub match ${match.id}`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
api.logger.warn(`claw-crony: failed to process hub match ${match.id} - ${err instanceof Error ? err.message : String(err)}`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
223
278
|
const plugin = {
|
|
224
279
|
id: "claw-crony",
|
|
225
280
|
name: "Claw Crony",
|
|
@@ -340,7 +395,9 @@ const plugin = {
|
|
|
340
395
|
let server = null;
|
|
341
396
|
let grpcServer = null;
|
|
342
397
|
let cleanupTimer = null;
|
|
398
|
+
let hubMatchPollingTimer = null;
|
|
343
399
|
const grpcPort = config.server.port + 1;
|
|
400
|
+
const processedHubMatches = new Set();
|
|
344
401
|
api.registerGatewayMethod("a2a.metrics", ({ respond }) => {
|
|
345
402
|
respond(true, {
|
|
346
403
|
metrics: telemetry.snapshot(),
|
|
@@ -548,7 +605,7 @@ const plugin = {
|
|
|
548
605
|
match = await client.createMatch({
|
|
549
606
|
skills: params.skills,
|
|
550
607
|
description: params.description,
|
|
551
|
-
token: client
|
|
608
|
+
token: getAdvertisedInboundToken(config, client) ?? client.registrationToken,
|
|
552
609
|
});
|
|
553
610
|
}
|
|
554
611
|
catch (err) {
|
|
@@ -561,7 +618,7 @@ const plugin = {
|
|
|
561
618
|
// Submit our token
|
|
562
619
|
let updatedMatch;
|
|
563
620
|
try {
|
|
564
|
-
updatedMatch = await client.submitToken(match.id, client
|
|
621
|
+
updatedMatch = await client.submitToken(match.id, getAdvertisedInboundToken(config, client) ?? client.registrationToken);
|
|
565
622
|
}
|
|
566
623
|
catch (err) {
|
|
567
624
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -572,24 +629,24 @@ const plugin = {
|
|
|
572
629
|
}
|
|
573
630
|
const provider = updatedMatch.provider;
|
|
574
631
|
const providerAddress = provider?.address ?? "(unknown)";
|
|
575
|
-
const
|
|
576
|
-
const
|
|
632
|
+
const providerAccessToken = updatedMatch.yourToken ?? "(none)";
|
|
633
|
+
const requesterInboundToken = updatedMatch.peerToken ?? "(none)";
|
|
577
634
|
const status = updatedMatch.status;
|
|
578
635
|
return {
|
|
579
636
|
content: [{
|
|
580
637
|
type: "text",
|
|
581
638
|
text: `Match ${status}: id=${updatedMatch.id}\n` +
|
|
582
639
|
`Provider: ${provider?.name ?? "(unknown)"} at ${providerAddress}\n` +
|
|
583
|
-
`
|
|
584
|
-
`
|
|
640
|
+
`Provider access token (use to contact provider): ${providerAccessToken}\n` +
|
|
641
|
+
`Requester inbound token (provider uses this to contact you): ${requesterInboundToken}`,
|
|
585
642
|
}],
|
|
586
643
|
details: {
|
|
587
644
|
ok: true,
|
|
588
645
|
matchId: updatedMatch.id,
|
|
589
646
|
status: updatedMatch.status,
|
|
590
647
|
providerAddress,
|
|
591
|
-
yourToken,
|
|
592
|
-
peerToken,
|
|
648
|
+
yourToken: providerAccessToken,
|
|
649
|
+
peerToken: requesterInboundToken,
|
|
593
650
|
},
|
|
594
651
|
};
|
|
595
652
|
},
|
|
@@ -682,6 +739,15 @@ const plugin = {
|
|
|
682
739
|
// Run once at startup to clear any backlog
|
|
683
740
|
doCleanup();
|
|
684
741
|
cleanupTimer = setInterval(doCleanup, intervalMs);
|
|
742
|
+
if (config.hub?.enabled !== false) {
|
|
743
|
+
const pollingIntervalMs = Math.max(5_000, config.resilience.healthCheck.intervalMs);
|
|
744
|
+
const pollHubMatches = () => {
|
|
745
|
+
void processPendingHubMatches(api, config, processedHubMatches);
|
|
746
|
+
};
|
|
747
|
+
pollHubMatches();
|
|
748
|
+
hubMatchPollingTimer = setInterval(pollHubMatches, pollingIntervalMs);
|
|
749
|
+
api.logger.info(`claw-crony: hub match polling enabled interval=${pollingIntervalMs}ms`);
|
|
750
|
+
}
|
|
685
751
|
api.logger.info(`claw-crony: task cleanup enabled — ttl=${config.storage.taskTtlHours}h interval=${config.storage.cleanupIntervalMinutes}min`);
|
|
686
752
|
},
|
|
687
753
|
async stop(_ctx) {
|
|
@@ -693,6 +759,10 @@ const plugin = {
|
|
|
693
759
|
clearInterval(cleanupTimer);
|
|
694
760
|
cleanupTimer = null;
|
|
695
761
|
}
|
|
762
|
+
if (hubMatchPollingTimer) {
|
|
763
|
+
clearInterval(hubMatchPollingTimer);
|
|
764
|
+
hubMatchPollingTimer = null;
|
|
765
|
+
}
|
|
696
766
|
// Stop gRPC server
|
|
697
767
|
if (grpcServer) {
|
|
698
768
|
grpcServer.forceShutdown();
|
package/dist/src/hub-match.d.ts
CHANGED
|
@@ -18,16 +18,23 @@ export interface HubAgentDto {
|
|
|
18
18
|
}
|
|
19
19
|
export interface HubMatchResult {
|
|
20
20
|
id: number;
|
|
21
|
+
requestId?: number | null;
|
|
21
22
|
status: string;
|
|
22
23
|
requester: HubAgentDto | null;
|
|
23
24
|
provider: HubAgentDto | null;
|
|
24
25
|
yourToken: string | null;
|
|
25
26
|
peerToken: string | null;
|
|
27
|
+
callerRole?: "requester" | "provider" | "observer" | null;
|
|
28
|
+
requesterTokenSubmitted?: boolean;
|
|
29
|
+
providerTokenSubmitted?: boolean;
|
|
30
|
+
readyForComplete?: boolean;
|
|
26
31
|
}
|
|
27
32
|
export declare class HubMatchClient {
|
|
28
33
|
private readonly hubUrl;
|
|
29
34
|
private readonly registration;
|
|
30
35
|
constructor(hubUrl: string, registration: HubRegistrationData);
|
|
36
|
+
get agentId(): number;
|
|
37
|
+
get registrationToken(): string;
|
|
31
38
|
static create(): Promise<HubMatchClient>;
|
|
32
39
|
private request;
|
|
33
40
|
/**
|
package/dist/src/hub-match.js
CHANGED
|
@@ -20,6 +20,12 @@ export class HubMatchClient {
|
|
|
20
20
|
this.hubUrl = hubUrl.replace(/\/$/, "");
|
|
21
21
|
this.registration = registration;
|
|
22
22
|
}
|
|
23
|
+
get agentId() {
|
|
24
|
+
return this.registration.agentId;
|
|
25
|
+
}
|
|
26
|
+
get registrationToken() {
|
|
27
|
+
return this.registration.token;
|
|
28
|
+
}
|
|
23
29
|
static async create() {
|
|
24
30
|
const registration = loadRegistration();
|
|
25
31
|
if (!registration) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { AgentSkillConfig } from "./types.js";
|
|
2
|
+
export declare const PRESET_AGENT_SKILLS: readonly ["chat", "search", "reasoning", "tool_use", "file_transfer", "image_understanding", "audio_understanding", "translation", "summarization", "ocr", "code_generation", "code_review", "data_analysis"];
|
|
3
|
+
export declare function normalizeConfiguredSkillName(value: string): string;
|
|
4
|
+
export declare function normalizeAgentCardSkills(rawSkills: Array<AgentSkillConfig | string> | undefined): Array<AgentSkillConfig | string>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export const PRESET_AGENT_SKILLS = [
|
|
2
|
+
"chat",
|
|
3
|
+
"search",
|
|
4
|
+
"reasoning",
|
|
5
|
+
"tool_use",
|
|
6
|
+
"file_transfer",
|
|
7
|
+
"image_understanding",
|
|
8
|
+
"audio_understanding",
|
|
9
|
+
"translation",
|
|
10
|
+
"summarization",
|
|
11
|
+
"ocr",
|
|
12
|
+
"code_generation",
|
|
13
|
+
"code_review",
|
|
14
|
+
"data_analysis",
|
|
15
|
+
];
|
|
16
|
+
export function normalizeConfiguredSkillName(value) {
|
|
17
|
+
return value
|
|
18
|
+
.trim()
|
|
19
|
+
.toLowerCase()
|
|
20
|
+
.replace(/[\s-]+/g, "_")
|
|
21
|
+
.replace(/_+/g, "_");
|
|
22
|
+
}
|
|
23
|
+
export function normalizeAgentCardSkills(rawSkills) {
|
|
24
|
+
if (!rawSkills || rawSkills.length === 0) {
|
|
25
|
+
return [{ id: "chat", name: "chat", description: "Chat bridge" }];
|
|
26
|
+
}
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
const normalized = [];
|
|
29
|
+
for (const entry of rawSkills) {
|
|
30
|
+
if (typeof entry === "string") {
|
|
31
|
+
const normalizedName = normalizeConfiguredSkillName(entry);
|
|
32
|
+
if (!normalizedName || seen.has(normalizedName)) {
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
seen.add(normalizedName);
|
|
36
|
+
normalized.push(normalizedName);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const normalizedName = normalizeConfiguredSkillName(entry.name);
|
|
40
|
+
if (!normalizedName || seen.has(normalizedName)) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
seen.add(normalizedName);
|
|
44
|
+
normalized.push({
|
|
45
|
+
id: entry.id ? normalizeConfiguredSkillName(entry.id) : normalizedName,
|
|
46
|
+
name: normalizedName,
|
|
47
|
+
description: entry.description?.trim() || normalizedName,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return normalized.length > 0
|
|
51
|
+
? normalized
|
|
52
|
+
: [{ id: "chat", name: "chat", description: "Chat bridge" }];
|
|
53
|
+
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -26,13 +26,14 @@
|
|
|
26
26
|
"name": { "type": "string" },
|
|
27
27
|
"description": { "type": "string" },
|
|
28
28
|
"url": { "type": "string" },
|
|
29
|
-
"skills": {
|
|
30
|
-
"type": "array",
|
|
31
|
-
"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
|
|
29
|
+
"skills": {
|
|
30
|
+
"type": "array",
|
|
31
|
+
"description": "Agent skills sent to the Hub. Prefer preset names such as chat, search, reasoning, tool_use, file_transfer, image_understanding, audio_understanding, translation, summarization, ocr, code_generation, code_review, data_analysis. Custom skills are also allowed.",
|
|
32
|
+
"items": {
|
|
33
|
+
"oneOf": [
|
|
34
|
+
{ "type": "string" },
|
|
35
|
+
{
|
|
36
|
+
"type": "object",
|
|
36
37
|
"properties": {
|
|
37
38
|
"id": { "type": "string" },
|
|
38
39
|
"name": { "type": "string" },
|