gopherhole_openclaw_a2a 0.2.8 → 0.3.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * A2A Channel Plugin for Clawdbot
2
+ * A2A Channel Plugin for OpenClaw
3
3
  * Enables communication with other AI agents via A2A protocol
4
4
  */
5
5
  import { A2AConnectionManager } from './connection.js';
@@ -1,5 +1,5 @@
1
1
  /**
2
- * A2A Channel Plugin for Clawdbot
2
+ * A2A Channel Plugin for OpenClaw
3
3
  * Enables communication with other AI agents via A2A protocol
4
4
  */
5
5
  // Use minimal type imports - mostly self-contained
@@ -25,10 +25,9 @@ function resolveA2AAccount(opts) {
25
25
  accountId,
26
26
  name: config.agentName ?? 'A2A',
27
27
  enabled: config.enabled ?? false,
28
- configured: !!(config.bridgeUrl || (config.agents && config.agents.length > 0) || config.gopherhole?.enabled),
29
- agentId: config.agentId ?? 'clawdbot',
28
+ configured: !!(config.bridgeUrl && config.apiKey),
29
+ agentId: config.agentId ?? 'openclaw',
30
30
  bridgeUrl: config.bridgeUrl ?? null,
31
- agents: config.agents ?? [],
32
31
  config,
33
32
  };
34
33
  }
@@ -39,9 +38,9 @@ const meta = {
39
38
  detailLabel: 'A2A Protocol',
40
39
  docsPath: '/channels/a2a',
41
40
  docsLabel: 'a2a',
42
- blurb: 'Communicate with other AI agents via A2A protocol.',
41
+ blurb: 'Communicate with other AI agents via GopherHole A2A protocol.',
43
42
  systemImage: 'bubble.left.and.bubble.right',
44
- aliases: ['agent2agent'],
43
+ aliases: ['agent2agent', 'gopherhole'],
45
44
  order: 200,
46
45
  };
47
46
  export const a2aPlugin = {
@@ -98,8 +97,8 @@ export const a2aPlugin = {
98
97
  messaging: {
99
98
  normalizeTarget: (target) => target?.trim() ?? '',
100
99
  targetResolver: {
101
- looksLikeId: (id) => /^[a-z0-9_-]+$/i.test(id),
102
- hint: '<agentId>',
100
+ looksLikeId: (id) => /^[a-z0-9_@-]+$/i.test(id),
101
+ hint: '<agentId> (e.g. @memory, @echo)',
103
102
  },
104
103
  formatTargetDisplay: ({ target }) => target ?? '',
105
104
  },
@@ -108,7 +107,7 @@ export const a2aPlugin = {
108
107
  applyAccountName: ({ cfg }) => cfg,
109
108
  validateInput: ({ input }) => {
110
109
  if (!input.httpUrl && !input.customArgs) {
111
- return 'A2A requires --http-url (bridge URL) or agents configured in config.';
110
+ return 'A2A requires --http-url (bridge URL) or bridgeUrl + apiKey in config.';
112
111
  }
113
112
  return null;
114
113
  },
@@ -179,16 +178,18 @@ export const a2aPlugin = {
179
178
  lastError: snapshot.lastError ?? null,
180
179
  }),
181
180
  probeAccount: async () => ({ ok: connectionManager !== null }),
182
- buildAccountSnapshot: ({ account, runtime }) => {
183
- const agents = connectionManager?.listAgents() ?? [];
181
+ buildAccountSnapshot: async ({ account, runtime }) => {
182
+ const connectionStatus = connectionManager?.listAgents() ?? [];
183
+ const availableAgents = await connectionManager?.listAvailableAgents() ?? [];
184
184
  return {
185
185
  accountId: account.accountId,
186
186
  name: account.name,
187
187
  enabled: account.enabled,
188
188
  configured: account.configured,
189
189
  running: runtime?.running ?? false,
190
- connected: agents.some((a) => a.connected),
191
- agents,
190
+ connected: connectionStatus.some((a) => a.connected),
191
+ hubStatus: connectionStatus,
192
+ availableAgents,
192
193
  lastStartAt: runtime?.lastStartAt ?? null,
193
194
  lastStopAt: runtime?.lastStopAt ?? null,
194
195
  lastError: runtime?.lastError ?? null,
@@ -211,7 +212,7 @@ export const a2aPlugin = {
211
212
  .join('\n') ?? '';
212
213
  if (!text)
213
214
  return;
214
- // Route to Clawdbot's reply pipeline via gateway JSON-RPC
215
+ // Route to OpenClaw's reply pipeline via gateway JSON-RPC
215
216
  try {
216
217
  ctx.log?.info(`[a2a] Routing message from ${message.from}: "${text.slice(0, 100)}..."`);
217
218
  // Use chat.send to route the message through the agent
@@ -219,26 +220,14 @@ export const a2aPlugin = {
219
220
  const sessionKey = `agent:main:a2a:${message.from}`;
220
221
  const response = await sendChatMessage(sessionKey, text);
221
222
  ctx.log?.info(`[a2a] chat.send returned: ${response ? `text=${response.text?.slice(0, 50)}...` : 'null'}`);
222
- // Send response back to the agent
223
+ // Send response back to the agent via GopherHole
223
224
  if (response?.text) {
224
- // If message came via GopherHole, route response back through it
225
- if (agentId === 'gopherhole' && message.from) {
226
- connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, response.text, message.contextId);
227
- }
228
- else {
229
- connectionManager?.sendResponse(agentId, message.taskId, response.text, message.contextId);
230
- }
225
+ connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, response.text, message.contextId);
231
226
  }
232
227
  }
233
228
  catch (err) {
234
229
  ctx.log?.error(`[a2a] Error handling message:`, err);
235
- // If message came via GopherHole, route error back through it
236
- if (agentId === 'gopherhole' && message.from) {
237
- connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, `Error: ${err.message}`, message.contextId);
238
- }
239
- else {
240
- connectionManager?.sendResponse(agentId, message.taskId, `Error: ${err.message}`, message.contextId);
241
- }
230
+ connectionManager?.sendResponseViaGopherHole(message.from, message.taskId, `Error: ${err.message}`, message.contextId);
242
231
  }
243
232
  }
244
233
  });
@@ -49,7 +49,17 @@ export declare class A2AConnectionManager {
49
49
  */
50
50
  isGopherHoleConnected(): boolean;
51
51
  /**
52
- * List connected agents (just GopherHole for now)
52
+ * List available agents from GopherHole
53
+ * Fetches same-tenant agents + agents with approved access + public agents
54
+ */
55
+ listAvailableAgents(): Promise<Array<{
56
+ id: string;
57
+ name: string;
58
+ description?: string;
59
+ accessType: 'same-tenant' | 'public' | 'granted';
60
+ }>>;
61
+ /**
62
+ * List connection status (for backward compatibility)
53
63
  */
54
64
  listAgents(): Array<{
55
65
  id: string;
@@ -11,30 +11,29 @@ export class A2AConnectionManager {
11
11
  connected = false;
12
12
  constructor(config) {
13
13
  this.config = config;
14
- this.agentId = config.agentId ?? 'clawdbot';
14
+ this.agentId = config.agentId ?? 'openclaw';
15
15
  }
16
16
  setMessageHandler(handler) {
17
17
  this.messageHandler = handler;
18
18
  }
19
19
  async start() {
20
- // Connect to GopherHole if configured
21
- if (this.config.gopherhole?.enabled && this.config.gopherhole?.apiKey) {
20
+ // Connect to GopherHole if configured (flat config: enabled + apiKey)
21
+ if (this.config.enabled && this.config.apiKey) {
22
22
  await this.connectToGopherHole();
23
23
  }
24
24
  }
25
25
  async connectToGopherHole() {
26
- const gphConfig = this.config.gopherhole;
27
- const hubUrl = gphConfig.hubUrl || 'wss://gopherhole.ai/ws';
26
+ const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
28
27
  const timeoutMs = this.config.requestTimeoutMs ?? 180000;
29
28
  this.gopherhole = new GopherHole({
30
- apiKey: gphConfig.apiKey,
29
+ apiKey: this.config.apiKey,
31
30
  hubUrl,
32
31
  autoReconnect: true,
33
32
  reconnectDelay: this.config.reconnectIntervalMs ?? 5000,
34
33
  maxReconnectAttempts: 20,
35
34
  requestTimeout: timeoutMs,
36
35
  messageTimeout: timeoutMs,
37
- agentCard: gphConfig.agentCard ?? {
36
+ agentCard: this.config.agentCard ?? {
38
37
  name: this.config.agentName ?? 'OpenClaw',
39
38
  description: 'Personal AI assistant with tools, web search, browser control, and various skills',
40
39
  version: '0.1.0',
@@ -213,7 +212,42 @@ export class A2AConnectionManager {
213
212
  return this.connected && this.gopherhole?.connected === true;
214
213
  }
215
214
  /**
216
- * List connected agents (just GopherHole for now)
215
+ * List available agents from GopherHole
216
+ * Fetches same-tenant agents + agents with approved access + public agents
217
+ */
218
+ async listAvailableAgents() {
219
+ if (!this.config.apiKey) {
220
+ return [];
221
+ }
222
+ const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
223
+ // Convert wss:// to https:// for API calls
224
+ const apiBase = hubUrl.replace('wss://', 'https://').replace('/ws', '');
225
+ try {
226
+ const response = await fetch(`${apiBase}/api/agents/available`, {
227
+ headers: {
228
+ 'Authorization': `Bearer ${this.config.apiKey}`,
229
+ 'Content-Type': 'application/json',
230
+ },
231
+ });
232
+ if (!response.ok) {
233
+ console.error(`[a2a] Failed to fetch agents: ${response.status}`);
234
+ return [];
235
+ }
236
+ const data = await response.json();
237
+ return data.agents.map(a => ({
238
+ id: a.id,
239
+ name: a.name,
240
+ description: a.description,
241
+ accessType: a.access_type,
242
+ }));
243
+ }
244
+ catch (err) {
245
+ console.error('[a2a] Error fetching available agents:', err.message);
246
+ return [];
247
+ }
248
+ }
249
+ /**
250
+ * List connection status (for backward compatibility)
217
251
  */
218
252
  listAgents() {
219
253
  const agents = [];
@@ -39,26 +39,27 @@ export interface A2AResponse {
39
39
  status: string;
40
40
  from?: string;
41
41
  }
42
+ /**
43
+ * A2A Channel Config (flat structure)
44
+ *
45
+ * Example:
46
+ * {
47
+ * "channels": {
48
+ * "a2a": {
49
+ * "enabled": true,
50
+ * "bridgeUrl": "wss://gopherhole.ai/ws",
51
+ * "apiKey": "gph_your_api_key"
52
+ * }
53
+ * }
54
+ * }
55
+ */
42
56
  export interface A2AChannelConfig {
43
57
  enabled?: boolean;
58
+ bridgeUrl?: string;
59
+ apiKey?: string;
44
60
  agentId?: string;
45
61
  agentName?: string;
46
- bridgeUrl?: string;
47
- agents?: Array<{
48
- id: string;
49
- url: string;
50
- name?: string;
51
- }>;
52
- gopherhole?: {
53
- enabled?: boolean;
54
- apiKey: string;
55
- hubUrl?: string;
56
- requestTimeoutMs?: number;
57
- agentCard?: A2AAgentCard;
58
- };
59
- auth?: {
60
- token?: string;
61
- };
62
+ agentCard?: A2AAgentCard;
62
63
  reconnectIntervalMs?: number;
63
64
  requestTimeoutMs?: number;
64
65
  }
@@ -69,10 +70,5 @@ export interface ResolvedA2AAccount {
69
70
  configured: boolean;
70
71
  agentId: string;
71
72
  bridgeUrl: string | null;
72
- agents: Array<{
73
- id: string;
74
- url: string;
75
- name?: string;
76
- }>;
77
73
  config: A2AChannelConfig;
78
74
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gopherhole_openclaw_a2a",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
4
4
  "description": "GopherHole A2A plugin for OpenClaw - connect your AI agent to the GopherHole network",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/channel.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * A2A Channel Plugin for Clawdbot
2
+ * A2A Channel Plugin for OpenClaw
3
3
  * Enables communication with other AI agents via A2A protocol
4
4
  */
5
5
 
@@ -18,7 +18,7 @@ import type {
18
18
  } from './types.js';
19
19
 
20
20
  // Minimal runtime interface - what we actually need
21
- interface ClawdbotRuntime {
21
+ interface OpenClawRuntime {
22
22
  handleInbound(params: {
23
23
  channel: string;
24
24
  chatId: string;
@@ -32,14 +32,14 @@ interface ClawdbotRuntime {
32
32
 
33
33
  // Runtime state
34
34
  let connectionManager: A2AConnectionManager | null = null;
35
- let currentRuntime: ClawdbotRuntime | null = null;
35
+ let currentRuntime: OpenClawRuntime | null = null;
36
36
 
37
37
  export function setA2ARuntime(runtime: unknown): void {
38
- currentRuntime = runtime as ClawdbotRuntime;
38
+ currentRuntime = runtime as OpenClawRuntime;
39
39
  }
40
40
 
41
41
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
42
- type ClawdbotConfig = any;
42
+ type OpenClawConfig = any;
43
43
 
44
44
  // Minimal channel plugin interfaces (self-contained)
45
45
  interface ChannelAccountSnapshot {
@@ -85,12 +85,12 @@ type ChannelPlugin<T = any> = {
85
85
  };
86
86
  };
87
87
 
88
- function resolveA2AConfig(cfg: ClawdbotConfig): A2AChannelConfig {
88
+ function resolveA2AConfig(cfg: OpenClawConfig): A2AChannelConfig {
89
89
  return cfg?.channels?.a2a ?? {};
90
90
  }
91
91
 
92
92
  function resolveA2AAccount(opts: {
93
- cfg: ClawdbotConfig;
93
+ cfg: OpenClawConfig;
94
94
  accountId?: string;
95
95
  }): ResolvedA2AAccount {
96
96
  const config = resolveA2AConfig(opts.cfg);
@@ -100,10 +100,9 @@ function resolveA2AAccount(opts: {
100
100
  accountId,
101
101
  name: config.agentName ?? 'A2A',
102
102
  enabled: config.enabled ?? false,
103
- configured: !!(config.bridgeUrl || (config.agents && config.agents.length > 0) || config.gopherhole?.enabled),
104
- agentId: config.agentId ?? 'clawdbot',
103
+ configured: !!(config.bridgeUrl && config.apiKey),
104
+ agentId: config.agentId ?? 'openclaw',
105
105
  bridgeUrl: config.bridgeUrl ?? null,
106
- agents: config.agents ?? [],
107
106
  config,
108
107
  };
109
108
  }
@@ -115,9 +114,9 @@ const meta = {
115
114
  detailLabel: 'A2A Protocol',
116
115
  docsPath: '/channels/a2a',
117
116
  docsLabel: 'a2a',
118
- blurb: 'Communicate with other AI agents via A2A protocol.',
117
+ blurb: 'Communicate with other AI agents via GopherHole A2A protocol.',
119
118
  systemImage: 'bubble.left.and.bubble.right',
120
- aliases: ['agent2agent'],
119
+ aliases: ['agent2agent', 'gopherhole'],
121
120
  order: 200,
122
121
  };
123
122
 
@@ -136,10 +135,10 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
136
135
  config: {
137
136
  listAccountIds: () => [DEFAULT_ACCOUNT_ID],
138
137
  resolveAccount: (cfg, accountId) =>
139
- resolveA2AAccount({ cfg: cfg as ClawdbotConfig, accountId }),
138
+ resolveA2AAccount({ cfg: cfg as OpenClawConfig, accountId }),
140
139
  defaultAccountId: () => DEFAULT_ACCOUNT_ID,
141
140
  setAccountEnabled: ({ cfg, enabled }) => {
142
- const next = cfg as ClawdbotConfig;
141
+ const next = cfg as OpenClawConfig;
143
142
  return {
144
143
  ...next,
145
144
  channels: {
@@ -149,9 +148,9 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
149
148
  enabled,
150
149
  },
151
150
  },
152
- } as ClawdbotConfig;
151
+ } as OpenClawConfig;
153
152
  },
154
- deleteAccount: ({ cfg }) => cfg as ClawdbotConfig,
153
+ deleteAccount: ({ cfg }) => cfg as OpenClawConfig,
155
154
  isConfigured: (account) => account.configured,
156
155
  describeAccount: (account): ChannelAccountSnapshot => ({
157
156
  accountId: account.accountId,
@@ -176,22 +175,22 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
176
175
  messaging: {
177
176
  normalizeTarget: (target) => target?.trim() ?? '',
178
177
  targetResolver: {
179
- looksLikeId: (id) => /^[a-z0-9_-]+$/i.test(id),
180
- hint: '<agentId>',
178
+ looksLikeId: (id) => /^[a-z0-9_@-]+$/i.test(id),
179
+ hint: '<agentId> (e.g. @memory, @echo)',
181
180
  },
182
181
  formatTargetDisplay: ({ target }) => target ?? '',
183
182
  },
184
183
  setup: {
185
184
  resolveAccountId: ({ accountId }) => normalizeAccountId(accountId),
186
- applyAccountName: ({ cfg }) => cfg as ClawdbotConfig,
185
+ applyAccountName: ({ cfg }) => cfg as OpenClawConfig,
187
186
  validateInput: ({ input }) => {
188
187
  if (!input.httpUrl && !input.customArgs) {
189
- return 'A2A requires --http-url (bridge URL) or agents configured in config.';
188
+ return 'A2A requires --http-url (bridge URL) or bridgeUrl + apiKey in config.';
190
189
  }
191
190
  return null;
192
191
  },
193
192
  applyAccountConfig: ({ cfg, input }) => {
194
- const next = cfg as ClawdbotConfig;
193
+ const next = cfg as OpenClawConfig;
195
194
  return {
196
195
  ...next,
197
196
  channels: {
@@ -202,7 +201,7 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
202
201
  ...(input.httpUrl ? { bridgeUrl: input.httpUrl } : {}),
203
202
  },
204
203
  },
205
- } as ClawdbotConfig;
204
+ } as OpenClawConfig;
206
205
  },
207
206
  },
208
207
  outbound: {
@@ -256,16 +255,18 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
256
255
  lastError: snapshot.lastError ?? null,
257
256
  }),
258
257
  probeAccount: async () => ({ ok: connectionManager !== null }),
259
- buildAccountSnapshot: ({ account, runtime }) => {
260
- const agents = connectionManager?.listAgents() ?? [];
258
+ buildAccountSnapshot: async ({ account, runtime }) => {
259
+ const connectionStatus = connectionManager?.listAgents() ?? [];
260
+ const availableAgents = await connectionManager?.listAvailableAgents() ?? [];
261
261
  return {
262
262
  accountId: account.accountId,
263
263
  name: account.name,
264
264
  enabled: account.enabled,
265
265
  configured: account.configured,
266
266
  running: runtime?.running ?? false,
267
- connected: agents.some((a) => a.connected),
268
- agents,
267
+ connected: connectionStatus.some((a) => a.connected),
268
+ hubStatus: connectionStatus,
269
+ availableAgents,
269
270
  lastStartAt: runtime?.lastStartAt ?? null,
270
271
  lastStopAt: runtime?.lastStopAt ?? null,
271
272
  lastError: runtime?.lastError ?? null,
@@ -292,7 +293,7 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
292
293
 
293
294
  if (!text) return;
294
295
 
295
- // Route to Clawdbot's reply pipeline via gateway JSON-RPC
296
+ // Route to OpenClaw's reply pipeline via gateway JSON-RPC
296
297
  try {
297
298
  ctx.log?.info(`[a2a] Routing message from ${message.from}: "${text.slice(0, 100)}..."`);
298
299
 
@@ -303,43 +304,23 @@ export const a2aPlugin: ChannelPlugin<ResolvedA2AAccount> = {
303
304
 
304
305
  ctx.log?.info(`[a2a] chat.send returned: ${response ? `text=${response.text?.slice(0, 50)}...` : 'null'}`);
305
306
 
306
- // Send response back to the agent
307
+ // Send response back to the agent via GopherHole
307
308
  if (response?.text) {
308
- // If message came via GopherHole, route response back through it
309
- if (agentId === 'gopherhole' && message.from) {
310
- connectionManager?.sendResponseViaGopherHole(
311
- message.from,
312
- message.taskId,
313
- response.text,
314
- message.contextId
315
- );
316
- } else {
317
- connectionManager?.sendResponse(
318
- agentId,
319
- message.taskId,
320
- response.text,
321
- message.contextId
322
- );
323
- }
324
- }
325
- } catch (err) {
326
- ctx.log?.error(`[a2a] Error handling message:`, err);
327
- // If message came via GopherHole, route error back through it
328
- if (agentId === 'gopherhole' && message.from) {
329
309
  connectionManager?.sendResponseViaGopherHole(
330
310
  message.from,
331
311
  message.taskId,
332
- `Error: ${(err as Error).message}`,
333
- message.contextId
334
- );
335
- } else {
336
- connectionManager?.sendResponse(
337
- agentId,
338
- message.taskId,
339
- `Error: ${(err as Error).message}`,
312
+ response.text,
340
313
  message.contextId
341
314
  );
342
315
  }
316
+ } catch (err) {
317
+ ctx.log?.error(`[a2a] Error handling message:`, err);
318
+ connectionManager?.sendResponseViaGopherHole(
319
+ message.from,
320
+ message.taskId,
321
+ `Error: ${(err as Error).message}`,
322
+ message.contextId
323
+ );
343
324
  }
344
325
  }
345
326
  });
package/src/connection.ts CHANGED
@@ -22,7 +22,7 @@ export class A2AConnectionManager {
22
22
 
23
23
  constructor(config: A2AChannelConfig) {
24
24
  this.config = config;
25
- this.agentId = config.agentId ?? 'clawdbot';
25
+ this.agentId = config.agentId ?? 'openclaw';
26
26
  }
27
27
 
28
28
  setMessageHandler(handler: MessageHandler): void {
@@ -30,26 +30,25 @@ export class A2AConnectionManager {
30
30
  }
31
31
 
32
32
  async start(): Promise<void> {
33
- // Connect to GopherHole if configured
34
- if (this.config.gopherhole?.enabled && this.config.gopherhole?.apiKey) {
33
+ // Connect to GopherHole if configured (flat config: enabled + apiKey)
34
+ if (this.config.enabled && this.config.apiKey) {
35
35
  await this.connectToGopherHole();
36
36
  }
37
37
  }
38
38
 
39
39
  private async connectToGopherHole(): Promise<void> {
40
- const gphConfig = this.config.gopherhole!;
41
- const hubUrl = gphConfig.hubUrl || 'wss://gopherhole.ai/ws';
40
+ const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
42
41
  const timeoutMs = this.config.requestTimeoutMs ?? 180000;
43
42
 
44
43
  this.gopherhole = new GopherHole({
45
- apiKey: gphConfig.apiKey,
44
+ apiKey: this.config.apiKey!,
46
45
  hubUrl,
47
46
  autoReconnect: true,
48
47
  reconnectDelay: this.config.reconnectIntervalMs ?? 5000,
49
48
  maxReconnectAttempts: 20,
50
49
  requestTimeout: timeoutMs,
51
50
  messageTimeout: timeoutMs,
52
- agentCard: gphConfig.agentCard ?? {
51
+ agentCard: this.config.agentCard ?? {
53
52
  name: this.config.agentName ?? 'OpenClaw',
54
53
  description: 'Personal AI assistant with tools, web search, browser control, and various skills',
55
54
  version: '0.1.0',
@@ -271,7 +270,57 @@ export class A2AConnectionManager {
271
270
  }
272
271
 
273
272
  /**
274
- * List connected agents (just GopherHole for now)
273
+ * List available agents from GopherHole
274
+ * Fetches same-tenant agents + agents with approved access + public agents
275
+ */
276
+ async listAvailableAgents(): Promise<Array<{
277
+ id: string;
278
+ name: string;
279
+ description?: string;
280
+ accessType: 'same-tenant' | 'public' | 'granted';
281
+ }>> {
282
+ if (!this.config.apiKey) {
283
+ return [];
284
+ }
285
+
286
+ const hubUrl = this.config.bridgeUrl || 'wss://gopherhole.ai/ws';
287
+ // Convert wss:// to https:// for API calls
288
+ const apiBase = hubUrl.replace('wss://', 'https://').replace('/ws', '');
289
+
290
+ try {
291
+ const response = await fetch(`${apiBase}/api/agents/available`, {
292
+ headers: {
293
+ 'Authorization': `Bearer ${this.config.apiKey}`,
294
+ 'Content-Type': 'application/json',
295
+ },
296
+ });
297
+
298
+ if (!response.ok) {
299
+ console.error(`[a2a] Failed to fetch agents: ${response.status}`);
300
+ return [];
301
+ }
302
+
303
+ const data = await response.json() as { agents: Array<{
304
+ id: string;
305
+ name: string;
306
+ description?: string;
307
+ access_type: string;
308
+ }> };
309
+
310
+ return data.agents.map(a => ({
311
+ id: a.id,
312
+ name: a.name,
313
+ description: a.description,
314
+ accessType: a.access_type as 'same-tenant' | 'public' | 'granted',
315
+ }));
316
+ } catch (err) {
317
+ console.error('[a2a] Error fetching available agents:', (err as Error).message);
318
+ return [];
319
+ }
320
+ }
321
+
322
+ /**
323
+ * List connection status (for backward compatibility)
275
324
  */
276
325
  listAgents(): Array<{ id: string; name: string; connected: boolean }> {
277
326
  const agents: Array<{ id: string; name: string; connected: boolean }> = [];
package/src/types.ts CHANGED
@@ -39,28 +39,29 @@ export interface A2AResponse {
39
39
  from?: string;
40
40
  }
41
41
 
42
+ /**
43
+ * A2A Channel Config (flat structure)
44
+ *
45
+ * Example:
46
+ * {
47
+ * "channels": {
48
+ * "a2a": {
49
+ * "enabled": true,
50
+ * "bridgeUrl": "wss://gopherhole.ai/ws",
51
+ * "apiKey": "gph_your_api_key"
52
+ * }
53
+ * }
54
+ * }
55
+ */
42
56
  export interface A2AChannelConfig {
43
57
  enabled?: boolean;
44
- agentId?: string; // Our agent ID (default: "clawdbot")
45
- agentName?: string; // Display name
46
- bridgeUrl?: string; // Legacy: direct bridge URL (ws://...)
47
- agents?: Array<{ // Legacy: direct agent connections
48
- id: string;
49
- url: string;
50
- name?: string;
51
- }>;
52
- gopherhole?: { // GopherHole Agent Hub connection
53
- enabled?: boolean;
54
- apiKey: string;
55
- hubUrl?: string; // Default: wss://gopherhole.ai/ws
56
- requestTimeoutMs?: number;
57
- agentCard?: A2AAgentCard; // Custom agent card (overrides defaults)
58
- };
59
- auth?: {
60
- token?: string;
61
- };
62
- reconnectIntervalMs?: number;
63
- requestTimeoutMs?: number;
58
+ bridgeUrl?: string; // WebSocket URL (default: wss://gopherhole.ai/ws)
59
+ apiKey?: string; // GopherHole API key (gph_...)
60
+ agentId?: string; // Our agent ID (default: "openclaw")
61
+ agentName?: string; // Display name for agent card
62
+ agentCard?: A2AAgentCard; // Custom agent card (overrides defaults)
63
+ reconnectIntervalMs?: number; // Reconnect delay (default: 5000)
64
+ requestTimeoutMs?: number; // Request timeout (default: 180000)
64
65
  }
65
66
 
66
67
  export interface ResolvedA2AAccount {
@@ -70,6 +71,5 @@ export interface ResolvedA2AAccount {
70
71
  configured: boolean;
71
72
  agentId: string;
72
73
  bridgeUrl: string | null;
73
- agents: Array<{ id: string; url: string; name?: string }>;
74
74
  config: A2AChannelConfig;
75
75
  }