@clawcrony/claw-crony 1.1.0 → 1.2.3

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
@@ -1,87 +1,95 @@
1
- # Claw Crony
2
-
3
- OpenClaw A2A v0.3.0 Gateway Auto-discovery and secure communication between OpenClaw Agents on different servers.
4
-
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
- [![A2A v0.3.0](https://img.shields.io/badge/A2A-v0.3.0-green.svg)](https://github.com/google/A2A)
7
-
8
- ## Key Features
9
-
10
- - **A2A Protocol v0.3.0** JSON-RPC / REST / gRPC with automatic fallback
11
- - **Hub Matchmaking** Auto-match peer Agents by skills with token exchange
12
- - **Smart Routing** Auto-select targets by message patterns, tags, or peer skills
13
- - **Secure Auth** Bearer Token + zero-downtime multi-token rotation
14
- - **Resilience** Health checks + exponential backoff + circuit breaker
15
- - **File Transfer** URI / base64 / MIME whitelist + SSRF protection
16
- - **Observability** JSONL audit logs + Telemetry metrics endpoint
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)
25
-
26
- ## Installation
27
-
28
- ### Via npm (Recommended)
29
-
30
- ```bash
31
- npm install @clawcrony/claw-crony
32
- ```
33
-
34
- ### Via Git Clone
35
-
36
- ```bash
37
- git clone https://github.com/ccccl8/claw-crony.git
38
- cd claw-crony
39
- npm install
40
- openclaw plugins install .
41
- openclaw gateway restart
42
-
43
- # Verify
44
- curl -s http://localhost:18800/.well-known/agent-card.json
45
- ```
46
-
47
- ## Adding a Peer
48
-
49
- ```bash
50
- openclaw config set plugins.entries.claw-crony.config.peers '[{
51
- "name": "Peer Name",
52
- "agentCardUrl": "http://<peerIP>:18800/.well-known/agent-card.json",
53
- "auth": { "type": "bearer", "token": "<peerToken>" }
54
- }]'
55
- openclaw gateway restart
56
- ```
57
-
58
- ## Hub Matchmaking (a2a_match_request)
59
-
60
- 发起匹配请求 to the Hub, which automatically finds registered Agents with the required skills:
61
-
62
- ```bash
63
- # Agent calls a2a_match_request tool with params:
64
- # { skills: ["chat"], description?: "optional description" }
65
- #
66
- # Returns: provider address + yourToken + peerToken
67
- # Both sides configure each other as peers using the returned tokens to communicate
68
- ```
69
-
70
- For detailed configuration steps, see [CONFIG.md](CONFIG.md).
71
-
72
- ## Endpoints
73
-
74
- | Endpoint | Method | Description |
75
- |----------|--------|-------------|
76
- | `/.well-known/agent-card.json` | GET | Agent Card (discovery) |
77
- | `/a2a/jsonrpc` | POST | A2A JSON-RPC |
78
- | `/a2a/rest` | POST | A2A REST transport |
79
- | `/a2a/metrics` | GET | Telemetry snapshot (when enabled) |
80
-
81
- ## License
82
-
83
- MIT License
84
-
85
- ## Acknowledgments
86
-
87
- This project is based on [win4r/openclaw-a2a-gateway](https://github.com/win4r/openclaw-a2a-gateway), MIT License.
1
+ # Claw Crony
2
+
3
+ OpenClaw A2A v0.3.0 Gateway - Auto-discovery and secure communication between OpenClaw Agents on different servers.
4
+
5
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
6
+ [![A2A v0.3.0](https://img.shields.io/badge/A2A-v0.3.0-green.svg)](https://github.com/google/A2A)
7
+
8
+ ## Key Features
9
+
10
+ - **A2A Protocol v0.3.0** - JSON-RPC / REST / gRPC with automatic fallback
11
+ - **Hub Matchmaking** - Auto-match peer Agents by skills with token exchange
12
+ - **Smart Routing** - Auto-select targets by message patterns, tags, or peer skills
13
+ - **Secure Auth** - Bearer Token + zero-downtime multi-token rotation
14
+ - **Resilience** - Health checks + exponential backoff + circuit breaker
15
+ - **File Transfer** - URI / base64 / MIME whitelist + SSRF protection
16
+ - **Observability** - JSONL audit logs + Telemetry metrics endpoint
17
+
18
+ ## Hub Server
19
+
20
+ Default Hub: `https://www.clawcrony.com`
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)
33
+
34
+ ## Installation
35
+
36
+ ### Via npm (Recommended)
37
+
38
+ ```bash
39
+ npm install @clawcrony/claw-crony
40
+ ```
41
+
42
+ ### Via Git Clone
43
+
44
+ ```bash
45
+ git clone https://github.com/ccccl8/claw-crony.git
46
+ cd claw-crony
47
+ npm install
48
+ openclaw plugins install .
49
+ openclaw gateway restart
50
+
51
+ # Verify
52
+ curl -s http://localhost:18800/.well-known/agent-card.json
53
+ ```
54
+
55
+ ## Adding a Peer
56
+
57
+ ```bash
58
+ openclaw config set plugins.entries.claw-crony.config.peers '[{
59
+ "name": "Peer Name",
60
+ "agentCardUrl": "http://<peerIP>:18800/.well-known/agent-card.json",
61
+ "auth": { "type": "bearer", "token": "<peerToken>" }
62
+ }]'
63
+ openclaw gateway restart
64
+ ```
65
+
66
+ ## Hub Matchmaking (a2a_match_request)
67
+
68
+ Send a matchmaking request to the Hub, which automatically finds registered Agents with the required skills:
69
+
70
+ ```bash
71
+ # Agent calls a2a_match_request tool with params:
72
+ # { skills: ["chat"], description?: "optional description" }
73
+ #
74
+ # Returns: provider address + yourToken + peerToken
75
+ # Both sides configure each other as peers using the returned tokens to communicate
76
+ ```
77
+
78
+ For detailed configuration steps, see [CONFIG.md](CONFIG.md).
79
+
80
+ ## Endpoints
81
+
82
+ | Endpoint | Method | Description |
83
+ |----------|--------|-------------|
84
+ | `/.well-known/agent-card.json` | GET | Agent Card (discovery) |
85
+ | `/a2a/jsonrpc` | POST | A2A JSON-RPC |
86
+ | `/a2a/rest` | POST | A2A REST transport |
87
+ | `/a2a/metrics` | GET | Telemetry snapshot (when enabled) |
88
+
89
+ ## License
90
+
91
+ MIT License
92
+
93
+ ## Acknowledgments
94
+
95
+ This project is based on [win4r/openclaw-a2a-gateway](https://github.com/win4r/openclaw-a2a-gateway), MIT License.
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"),
@@ -203,7 +204,7 @@ export function parseConfig(raw, resolvePath) {
203
204
  },
204
205
  },
205
206
  hub: {
206
- url: asString(hub.url, "https://www.factormining.cn"),
207
+ url: asString(hub.url, "https://www.clawcrony.com"),
207
208
  enabled: asBoolean(hub.enabled, true),
208
209
  registrationEnabled: asBoolean(hub.registrationEnabled, true),
209
210
  },
@@ -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) {
@@ -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
+ }
@@ -2,18 +2,18 @@
2
2
  "id": "claw-crony",
3
3
  "name": "Claw Crony",
4
4
  "description": "OpenClaw A2A v0.3.0 gateway with Agent Card, JSON-RPC, REST, routing rules, transport fallback, and Hub matchmaking",
5
- "version": "1.0.1",
5
+ "version": "1.2.3",
6
6
  "defaultConfig": {
7
7
  "agentCard": {
8
8
  "name": "OpenClaw A2A Gateway",
9
9
  "description": "A2A bridge for OpenClaw agents",
10
10
  "skills": [{ "id": "chat", "name": "chat", "description": "Chat bridge" }]
11
- },
12
- "hub": {
13
- "url": "https://www.factormining.cn",
14
- "enabled": true,
15
- "registrationEnabled": true
16
- }
11
+ },
12
+ "hub": {
13
+ "url": "https://www.clawcrony.com",
14
+ "enabled": true,
15
+ "registrationEnabled": true
16
+ }
17
17
  },
18
18
  "configSchema": {
19
19
  "type": "object",
@@ -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.1.0",
3
+ "version": "1.2.3",
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",