@clawcrony/claw-crony 1.0.4 → 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 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发起匹配请求, and the Hub will return available peer Agent addresses and auth tokens based on skills.
23
-
24
- A2A service port: **18800** (default)
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
- 发起匹配请求 to the Hub, which automatically finds registered Agents with the required skills:
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["registration"].token,
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["registration"].token);
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 yourToken = updatedMatch.yourToken ?? "(none)";
576
- const peerToken = updatedMatch.peerToken ?? "(none)";
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
- `Your token (use to contact provider): ${yourToken}\n` +
584
- `Peer token (provider's token to contact you): ${peerToken}`,
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();
@@ -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
  /**
@@ -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) {
@@ -231,6 +231,10 @@ export async function runHubRegistration(api, config, hubConfig, registrationCon
231
231
  // Non-fatal — agent is registered, web login may already exist
232
232
  }
233
233
  }
234
+ else {
235
+ api.logger.info(`claw-crony: Agent registered with hub (agentId=${agentId}). ` +
236
+ `Visit ${hubUrl}/register to create your web dashboard account.`);
237
+ }
234
238
  }
235
239
  catch (err) {
236
240
  // 409 Conflict: address already registered by someone else — try to find our agentId
@@ -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
+ }
@@ -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
- "items": {
32
- "oneOf": [
33
- { "type": "string" },
34
- {
35
- "type": "object",
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" },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawcrony/claw-crony",
3
- "version": "1.0.4",
3
+ "version": "1.2.2",
4
4
  "type": "module",
5
5
  "description": "OpenClaw A2A gateway plugin implementing the A2A v0.3.0 protocol surface",
6
6
  "main": "dist/index.js",